[{"data":1,"prerenderedAt":668},["ShallowReactive",2],{"blog-/blog/nuxt-3-migration":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"tags":11,"body":14,"_type":662,"_id":663,"_source":664,"_file":665,"_stem":666,"_extension":667},"/blog/nuxt-3-migration","blog",false,"","Migrating from Nuxt 2 to Nuxt 3","What I learned rebuilding this site from scratch — and why the move was worth it.","2025-03-10",[12,13],"nuxt","web development",{"type":15,"children":16,"toc":656},"root",[17,25,32,37,82,95,101,121,134,140,145,217,246,252,273,394,399,645,650],{"type":18,"tag":19,"props":20,"children":21},"element","p",{},[22],{"type":23,"value":24},"text","This website used to run on Nuxt 2. Single page, no blog, no components — just text and a few logos. Simple, but the framework hit end-of-life at the end of 2024. Time for a proper rebuild.",{"type":18,"tag":26,"props":27,"children":29},"h2",{"id":28},"what-changed",[30],{"type":23,"value":31},"What changed",{"type":18,"tag":19,"props":33,"children":34},{},[35],{"type":23,"value":36},"Three things motivated the move:",{"type":18,"tag":38,"props":39,"children":40},"ol",{},[41,53,63],{"type":18,"tag":42,"props":43,"children":44},"li",{},[45,51],{"type":18,"tag":46,"props":47,"children":48},"strong",{},[49],{"type":23,"value":50},"Get onto a maintained framework",{"type":23,"value":52}," — Nuxt 2 is no longer receiving security updates",{"type":18,"tag":42,"props":54,"children":55},{},[56,61],{"type":18,"tag":46,"props":57,"children":58},{},[59],{"type":23,"value":60},"Add a blog without a CMS",{"type":23,"value":62}," — I wanted to write posts as Markdown files, not manage a database",{"type":18,"tag":42,"props":64,"children":65},{},[66,71,73,80],{"type":18,"tag":46,"props":67,"children":68},{},[69],{"type":23,"value":70},"Refresh the design",{"type":23,"value":72}," — the old site had no visual hierarchy, minimal accessibility, and a broken SEO setup (it was a client-side-only SPA with ",{"type":18,"tag":74,"props":75,"children":77},"code",{"className":76},[],[78],{"type":23,"value":79},"ssr: false",{"type":23,"value":81},")",{"type":18,"tag":19,"props":83,"children":84},{},[85,87,93],{"type":23,"value":86},"Nuxt 3 with ",{"type":18,"tag":74,"props":88,"children":90},{"className":89},[],[91],{"type":23,"value":92},"@nuxt/content",{"type":23,"value":94}," ticks all three boxes.",{"type":18,"tag":26,"props":96,"children":98},{"id":97},"the-seo-problem-with-the-old-site",[99],{"type":23,"value":100},"The SEO problem with the old site",{"type":18,"tag":19,"props":102,"children":103},{},[104,106,111,113,119],{"type":23,"value":105},"The old config had ",{"type":18,"tag":74,"props":107,"children":109},{"className":108},[],[110],{"type":23,"value":79},{"type":23,"value":112},", which means Nuxt rendered a blank HTML shell on the server and populated it in the browser. Google ",{"type":18,"tag":114,"props":115,"children":116},"em",{},[117],{"type":23,"value":118},"can",{"type":23,"value":120}," crawl SPAs, but it's unreliable and slow — the crawler needs to execute JavaScript before it can index any content.",{"type":18,"tag":19,"props":122,"children":123},{},[124,126,132],{"type":23,"value":125},"With ",{"type":18,"tag":74,"props":127,"children":129},{"className":128},[],[130],{"type":23,"value":131},"nuxt generate",{"type":23,"value":133}," in Nuxt 3 (SSR enabled at build time), every page ships as fully pre-rendered HTML. Google reads it instantly, no JavaScript execution needed.",{"type":18,"tag":26,"props":135,"children":137},{"id":136},"the-migration",[138],{"type":23,"value":139},"The migration",{"type":18,"tag":19,"props":141,"children":142},{},[143],{"type":23,"value":144},"For a site this small, the migration was straightforward:",{"type":18,"tag":146,"props":147,"children":148},"ul",{},[149,168,194,212],{"type":18,"tag":42,"props":150,"children":151},{},[152,158,160,166],{"type":18,"tag":74,"props":153,"children":155},{"className":154},[],[156],{"type":23,"value":157},"nuxt.config.js",{"type":23,"value":159}," → ",{"type":18,"tag":74,"props":161,"children":163},{"className":162},[],[164],{"type":23,"value":165},"nuxt.config.ts",{"type":23,"value":167}," — typed config, new module API",{"type":18,"tag":42,"props":169,"children":170},{},[171,177,178,184,186,192],{"type":18,"tag":74,"props":172,"children":174},{"className":173},[],[175],{"type":23,"value":176},"\u003CNuxt />",{"type":23,"value":159},{"type":18,"tag":74,"props":179,"children":181},{"className":180},[],[182],{"type":23,"value":183},"\u003Cslot />",{"type":23,"value":185}," in layouts (no more ",{"type":18,"tag":74,"props":187,"children":189},{"className":188},[],[190],{"type":23,"value":191},"\u003CNuxtPage />",{"type":23,"value":193}," needed in layouts)",{"type":18,"tag":42,"props":195,"children":196},{},[197,203,204,210],{"type":18,"tag":74,"props":198,"children":200},{"className":199},[],[201],{"type":23,"value":202},"asyncData",{"type":23,"value":159},{"type":18,"tag":74,"props":205,"children":207},{"className":206},[],[208],{"type":23,"value":209},"useAsyncData",{"type":23,"value":211}," composable",{"type":18,"tag":42,"props":213,"children":214},{},[215],{"type":23,"value":216},"Global styles from SASS to plain CSS with Tailwind directives",{"type":18,"tag":19,"props":218,"children":219},{},[220,222,228,230,236,238,244],{"type":23,"value":221},"The trickiest part was getting ",{"type":18,"tag":74,"props":223,"children":225},{"className":224},[],[226],{"type":23,"value":227},"@tailwindcss/typography",{"type":23,"value":229}," right for dark mode. The ",{"type":18,"tag":74,"props":231,"children":233},{"className":232},[],[234],{"type":23,"value":235},"prose-invert",{"type":23,"value":237}," modifier handles most of it, but code block colours needed manual ",{"type":18,"tag":74,"props":239,"children":241},{"className":240},[],[242],{"type":23,"value":243},"--tw-prose-*",{"type":23,"value":245}," CSS variable overrides.",{"type":18,"tag":26,"props":247,"children":249},{"id":248},"nuxtcontent-is-excellent",[250],{"type":23,"value":251},"@nuxt/content is excellent",{"type":18,"tag":19,"props":253,"children":254},{},[255,257,263,265,271],{"type":23,"value":256},"Writing posts as ",{"type":18,"tag":74,"props":258,"children":260},{"className":259},[],[261],{"type":23,"value":262},".md",{"type":23,"value":264}," files in a ",{"type":18,"tag":74,"props":266,"children":268},{"className":267},[],[269],{"type":23,"value":270},"content/",{"type":23,"value":272}," folder is the right workflow for a developer blog. Frontmatter handles metadata (title, description, date, tags) cleanly. Shiki handles syntax highlighting out of the box:",{"type":18,"tag":274,"props":275,"children":279},"pre",{"className":276,"code":277,"language":278,"meta":7,"style":7},"language-typescript shiki shiki-themes github-dark","// This is automatically highlighted\nconst greeting = (name: string): string => {\n  return `Hello, ${name}!`\n}\n","typescript",[280],{"type":18,"tag":74,"props":281,"children":282},{"__ignoreMap":7},[283,295,361,385],{"type":18,"tag":284,"props":285,"children":288},"span",{"class":286,"line":287},"line",1,[289],{"type":18,"tag":284,"props":290,"children":292},{"style":291},"--shiki-default:#6A737D",[293],{"type":23,"value":294},"// This is automatically highlighted\n",{"type":18,"tag":284,"props":296,"children":298},{"class":286,"line":297},2,[299,305,311,316,322,328,333,339,343,347,351,356],{"type":18,"tag":284,"props":300,"children":302},{"style":301},"--shiki-default:#F97583",[303],{"type":23,"value":304},"const",{"type":18,"tag":284,"props":306,"children":308},{"style":307},"--shiki-default:#B392F0",[309],{"type":23,"value":310}," greeting",{"type":18,"tag":284,"props":312,"children":313},{"style":301},[314],{"type":23,"value":315}," =",{"type":18,"tag":284,"props":317,"children":319},{"style":318},"--shiki-default:#E1E4E8",[320],{"type":23,"value":321}," (",{"type":18,"tag":284,"props":323,"children":325},{"style":324},"--shiki-default:#FFAB70",[326],{"type":23,"value":327},"name",{"type":18,"tag":284,"props":329,"children":330},{"style":301},[331],{"type":23,"value":332},":",{"type":18,"tag":284,"props":334,"children":336},{"style":335},"--shiki-default:#79B8FF",[337],{"type":23,"value":338}," string",{"type":18,"tag":284,"props":340,"children":341},{"style":318},[342],{"type":23,"value":81},{"type":18,"tag":284,"props":344,"children":345},{"style":301},[346],{"type":23,"value":332},{"type":18,"tag":284,"props":348,"children":349},{"style":335},[350],{"type":23,"value":338},{"type":18,"tag":284,"props":352,"children":353},{"style":301},[354],{"type":23,"value":355}," =>",{"type":18,"tag":284,"props":357,"children":358},{"style":318},[359],{"type":23,"value":360}," {\n",{"type":18,"tag":284,"props":362,"children":364},{"class":286,"line":363},3,[365,370,376,380],{"type":18,"tag":284,"props":366,"children":367},{"style":301},[368],{"type":23,"value":369},"  return",{"type":18,"tag":284,"props":371,"children":373},{"style":372},"--shiki-default:#9ECBFF",[374],{"type":23,"value":375}," `Hello, ${",{"type":18,"tag":284,"props":377,"children":378},{"style":318},[379],{"type":23,"value":327},{"type":18,"tag":284,"props":381,"children":382},{"style":372},[383],{"type":23,"value":384},"}!`\n",{"type":18,"tag":284,"props":386,"children":388},{"class":286,"line":387},4,[389],{"type":18,"tag":284,"props":390,"children":391},{"style":318},[392],{"type":23,"value":393},"}\n",{"type":18,"tag":19,"props":395,"children":396},{},[397],{"type":23,"value":398},"The query API is also clean and composable:",{"type":18,"tag":274,"props":400,"children":404},{"className":401,"code":402,"language":403,"meta":7,"style":7},"language-javascript shiki shiki-themes github-dark","// Fetch 3 latest posts, pick only the fields you need\nconst { data } = await useAsyncData('posts', () =>\n  queryContent('/blog')\n    .sort({ date: -1 })\n    .limit(3)\n    .only(['title', 'description', 'date', 'tags', '_path'])\n    .find()\n)\n","javascript",[405],{"type":18,"tag":74,"props":406,"children":407},{"__ignoreMap":7},[408,416,473,495,528,554,619,637],{"type":18,"tag":284,"props":409,"children":410},{"class":286,"line":287},[411],{"type":18,"tag":284,"props":412,"children":413},{"style":291},[414],{"type":23,"value":415},"// Fetch 3 latest posts, pick only the fields you need\n",{"type":18,"tag":284,"props":417,"children":418},{"class":286,"line":297},[419,423,428,433,438,443,448,453,458,463,468],{"type":18,"tag":284,"props":420,"children":421},{"style":301},[422],{"type":23,"value":304},{"type":18,"tag":284,"props":424,"children":425},{"style":318},[426],{"type":23,"value":427}," { ",{"type":18,"tag":284,"props":429,"children":430},{"style":335},[431],{"type":23,"value":432},"data",{"type":18,"tag":284,"props":434,"children":435},{"style":318},[436],{"type":23,"value":437}," } ",{"type":18,"tag":284,"props":439,"children":440},{"style":301},[441],{"type":23,"value":442},"=",{"type":18,"tag":284,"props":444,"children":445},{"style":301},[446],{"type":23,"value":447}," await",{"type":18,"tag":284,"props":449,"children":450},{"style":307},[451],{"type":23,"value":452}," useAsyncData",{"type":18,"tag":284,"props":454,"children":455},{"style":318},[456],{"type":23,"value":457},"(",{"type":18,"tag":284,"props":459,"children":460},{"style":372},[461],{"type":23,"value":462},"'posts'",{"type":18,"tag":284,"props":464,"children":465},{"style":318},[466],{"type":23,"value":467},", () ",{"type":18,"tag":284,"props":469,"children":470},{"style":301},[471],{"type":23,"value":472},"=>\n",{"type":18,"tag":284,"props":474,"children":475},{"class":286,"line":363},[476,481,485,490],{"type":18,"tag":284,"props":477,"children":478},{"style":307},[479],{"type":23,"value":480},"  queryContent",{"type":18,"tag":284,"props":482,"children":483},{"style":318},[484],{"type":23,"value":457},{"type":18,"tag":284,"props":486,"children":487},{"style":372},[488],{"type":23,"value":489},"'/blog'",{"type":18,"tag":284,"props":491,"children":492},{"style":318},[493],{"type":23,"value":494},")\n",{"type":18,"tag":284,"props":496,"children":497},{"class":286,"line":387},[498,503,508,513,518,523],{"type":18,"tag":284,"props":499,"children":500},{"style":318},[501],{"type":23,"value":502},"    .",{"type":18,"tag":284,"props":504,"children":505},{"style":307},[506],{"type":23,"value":507},"sort",{"type":18,"tag":284,"props":509,"children":510},{"style":318},[511],{"type":23,"value":512},"({ date: ",{"type":18,"tag":284,"props":514,"children":515},{"style":301},[516],{"type":23,"value":517},"-",{"type":18,"tag":284,"props":519,"children":520},{"style":335},[521],{"type":23,"value":522},"1",{"type":18,"tag":284,"props":524,"children":525},{"style":318},[526],{"type":23,"value":527}," })\n",{"type":18,"tag":284,"props":529,"children":531},{"class":286,"line":530},5,[532,536,541,545,550],{"type":18,"tag":284,"props":533,"children":534},{"style":318},[535],{"type":23,"value":502},{"type":18,"tag":284,"props":537,"children":538},{"style":307},[539],{"type":23,"value":540},"limit",{"type":18,"tag":284,"props":542,"children":543},{"style":318},[544],{"type":23,"value":457},{"type":18,"tag":284,"props":546,"children":547},{"style":335},[548],{"type":23,"value":549},"3",{"type":18,"tag":284,"props":551,"children":552},{"style":318},[553],{"type":23,"value":494},{"type":18,"tag":284,"props":555,"children":557},{"class":286,"line":556},6,[558,562,567,572,577,582,587,591,596,600,605,609,614],{"type":18,"tag":284,"props":559,"children":560},{"style":318},[561],{"type":23,"value":502},{"type":18,"tag":284,"props":563,"children":564},{"style":307},[565],{"type":23,"value":566},"only",{"type":18,"tag":284,"props":568,"children":569},{"style":318},[570],{"type":23,"value":571},"([",{"type":18,"tag":284,"props":573,"children":574},{"style":372},[575],{"type":23,"value":576},"'title'",{"type":18,"tag":284,"props":578,"children":579},{"style":318},[580],{"type":23,"value":581},", ",{"type":18,"tag":284,"props":583,"children":584},{"style":372},[585],{"type":23,"value":586},"'description'",{"type":18,"tag":284,"props":588,"children":589},{"style":318},[590],{"type":23,"value":581},{"type":18,"tag":284,"props":592,"children":593},{"style":372},[594],{"type":23,"value":595},"'date'",{"type":18,"tag":284,"props":597,"children":598},{"style":318},[599],{"type":23,"value":581},{"type":18,"tag":284,"props":601,"children":602},{"style":372},[603],{"type":23,"value":604},"'tags'",{"type":18,"tag":284,"props":606,"children":607},{"style":318},[608],{"type":23,"value":581},{"type":18,"tag":284,"props":610,"children":611},{"style":372},[612],{"type":23,"value":613},"'_path'",{"type":18,"tag":284,"props":615,"children":616},{"style":318},[617],{"type":23,"value":618},"])\n",{"type":18,"tag":284,"props":620,"children":622},{"class":286,"line":621},7,[623,627,632],{"type":18,"tag":284,"props":624,"children":625},{"style":318},[626],{"type":23,"value":502},{"type":18,"tag":284,"props":628,"children":629},{"style":307},[630],{"type":23,"value":631},"find",{"type":18,"tag":284,"props":633,"children":634},{"style":318},[635],{"type":23,"value":636},"()\n",{"type":18,"tag":284,"props":638,"children":640},{"class":286,"line":639},8,[641],{"type":18,"tag":284,"props":642,"children":643},{"style":318},[644],{"type":23,"value":494},{"type":18,"tag":19,"props":646,"children":647},{},[648],{"type":23,"value":649},"Reading time is calculated automatically. Strongly recommended.",{"type":18,"tag":651,"props":652,"children":653},"style",{},[654],{"type":23,"value":655},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":297,"depth":297,"links":657},[658,659,660,661],{"id":28,"depth":297,"text":31},{"id":97,"depth":297,"text":100},{"id":136,"depth":297,"text":139},{"id":248,"depth":297,"text":251},"markdown","content:blog:nuxt-3-migration.md","content","blog/nuxt-3-migration.md","blog/nuxt-3-migration","md",1778670303420]