How I rebuilt my personal site with Nuxt 4 and Firebase
My old site was a single static HTML file. Here's how and why I rebuilt it with Nuxt 4, Tailwind CSS, Firebase Hosting, and a fully custom multilingual system covering 10 languages.
The old site
For a long time, my personal site was a single index.html file. Minimal, fast, and easy to maintain. It did the job — but as I started building more projects and wanted a proper news feed, it wasn't going to scale.
I decided to do a full rebuild.
Why Nuxt 4
I chose Nuxt 4 for a few specific reasons:
- Static generation —
nuxt generateproduces a fully static output compatible with Firebase Hosting - File-based routing — Article pages at
/news/[slug]just work with zero configuration - TypeScript-first — The entire codebase is typed, from server routes to composables
- SSR + hydration — Pages are pre-rendered on the server and hydrated on the client, which is ideal for SEO
The stack
- Nuxt 4 (compatibility version 4) — App directory structure, SSR with static generation
- Tailwind CSS — Utility-first styling with no component library overhead
- Firebase Hosting — Global CDN, near-zero config deploys with
firebase deploy - Custom i18n — A hand-rolled composable using
vue-i18ndirectly, no module needed - Nitro server routes — Articles are stored as local JSON files and served via
server/api/endpoints
The multilingual system
The site supports 10 languages: English, French, German, Spanish, Italian, Dutch, Norwegian, Polish, Portuguese, and Turkish.
I deliberately avoided @nuxtjs/i18n. For a personal site with this scope, the module brings too much weight and complexity. Instead, I wrote a useI18n() composable that wraps vue-i18n directly and reads from a locale cookie. Switching languages is instant and doesn't require a page reload.
Articles as local JSON
Each article is a single JSON file in content/articles/. The schema is simple:
slug— the URL keydate— ISO date stringtags— array of stringsurl— optional external linktranslations— an object with one entry per locale, each containingtitle,summary, andbody(Markdown text)
SEO and prerendering
For each article to be properly indexed, it needs its own static HTML file. A nitro:config hook reads the content/articles/ directory before the Vite build and adds every /news/[slug] route to the prerender list.
Each article page uses useSeoMeta() for meta tags and useHead() for canonical + hreflang links across all 10 locales. Google can index every article in every language from a single URL.
What I'd do differently
The main thing I underestimated was hydration. With SSR, any mismatch between what the server renders and what the client expects causes Vue hydration warnings. The most common culprit: conditional rendering based on client-only state (like a cookie or localStorage). The fix is wrapping those blocks in <ClientOnly> or initializing state carefully from the server.
What's next
The rebuild is complete. The site now has proper routing, a news feed, and full multilingual support. I plan to keep publishing here — new Reloadium launches, product thinking, and the occasional technical deep-dive.