[{"data":1,"prerenderedAt":1862},["ShallowReactive",2],{"posts":3},[4,1770],{"id":5,"title":6,"alt":7,"author":8,"body":11,"date":1757,"description":1758,"extension":1759,"img":7,"meta":1760,"navigation":307,"path":1761,"seo":1762,"stem":1763,"tags":1764,"updated":1768,"__hash__":1769},"blog\u002Fblog\u002Fnuxt3-content3-migration.md","Migrating to Nuxt 3 and Nuxt Content 3",null,{"name":9,"bio":10},"slpk","check la page about",{"type":12,"value":13,"toc":1730},"minimark",[14,18,22,27,30,64,67,71,74,123,127,134,227,241,263,267,281,568,571,599,603,608,735,739,890,893,942,946,953,959,1013,1033,1037,1040,1172,1175,1179,1182,1238,1245,1287,1291,1298,1310,1316,1474,1481,1535,1539,1543,1556,1567,1575,1585,1592,1598,1633,1636,1640,1648,1652,1655,1681,1685,1717,1720,1726],[15,16,6],"h1",{"id":17},"migrating-to-nuxt-3-and-nuxt-content-3",[19,20,21],"p",{},"After some time running my personal site on Nuxt 2 with the Content v2 module, I decided it was time to modernize the stack. Here's a comprehensive breakdown of what changed, what broke, and how I fixed it.",[23,24,26],"h2",{"id":25},"why-migrate","Why Migrate?",[19,28,29],{},"Nuxt 3 brings significant improvements:",[31,32,33,41,52,58],"ul",{},[34,35,36,40],"li",{},[37,38,39],"strong",{},"Better performance"," with the Nitro engine and auto-generated server functions",[34,42,43,46,47,51],{},[37,44,45],{},"Composition API by default"," with cleaner ",[48,49,50],"code",{},"\u003Cscript setup>"," syntax",[34,53,54,57],{},[37,55,56],{},"TypeScript native support"," without extra configuration",[34,59,60,63],{},[37,61,62],{},"Smaller bundle sizes"," thanks to tree-shaking and the new bundler",[19,65,66],{},"Nuxt Content 3 is a complete rewrite with SQL-backed queries, making content fetching dramatically faster and more flexible.",[23,68,70],{"id":69},"the-starting-point","The Starting Point",[19,72,73],{},"My site was running:",[31,75,76,85,91,105,112],{},[34,77,78,81,82],{},[37,79,80],{},"Nuxt 2.15"," with ",[48,83,84],{},"@nuxt\u002Fcontent@2.6.0",[34,86,87,90],{},[37,88,89],{},"Tailwind CSS"," for utility-first styling",[34,92,93,96,97,100,101,104],{},[37,94,95],{},"Custom canvas components"," for animated text (",[48,98,99],{},"MadjozoAnimation",", ",[48,102,103],{},"TextWave",")",[34,106,107,108,111],{},"A blog at ",[48,109,110],{},"\u002Fblog\u002F[slug]"," with tag and author filtering",[34,113,114,115,118,119,122],{},"A static ",[48,116,117],{},"\u002Fabout"," page and an ",[48,120,121],{},"\u002Finstagram"," links page",[23,124,126],{"id":125},"phase-1-dependency-upgrade","Phase 1: Dependency Upgrade",[19,128,129,130,133],{},"The first step was updating ",[48,131,132],{},"package.json",":",[135,136,141],"pre",{"className":137,"code":138,"language":139,"meta":140,"style":140},"language-json shiki shiki-themes github-light github-dark","{\n  \"devDependencies\": {\n    \"@nuxt\u002Fcontent\": \"^3.0.0\",\n    \"nuxt\": \"^3.15.0\",\n    \"typescript\": \"^5.7.0\",\n    \"zod\": \"^3.24.0\"\n  }\n}\n","json","",[48,142,143,152,162,178,191,204,215,221],{"__ignoreMap":140},[144,145,148],"span",{"class":146,"line":147},"line",1,[144,149,151],{"class":150},"sVt8B","{\n",[144,153,155,159],{"class":146,"line":154},2,[144,156,158],{"class":157},"sj4cs","  \"devDependencies\"",[144,160,161],{"class":150},": {\n",[144,163,165,168,171,175],{"class":146,"line":164},3,[144,166,167],{"class":157},"    \"@nuxt\u002Fcontent\"",[144,169,170],{"class":150},": ",[144,172,174],{"class":173},"sZZnC","\"^3.0.0\"",[144,176,177],{"class":150},",\n",[144,179,181,184,186,189],{"class":146,"line":180},4,[144,182,183],{"class":157},"    \"nuxt\"",[144,185,170],{"class":150},[144,187,188],{"class":173},"\"^3.15.0\"",[144,190,177],{"class":150},[144,192,194,197,199,202],{"class":146,"line":193},5,[144,195,196],{"class":157},"    \"typescript\"",[144,198,170],{"class":150},[144,200,201],{"class":173},"\"^5.7.0\"",[144,203,177],{"class":150},[144,205,207,210,212],{"class":146,"line":206},6,[144,208,209],{"class":157},"    \"zod\"",[144,211,170],{"class":150},[144,213,214],{"class":173},"\"^3.24.0\"\n",[144,216,218],{"class":146,"line":217},7,[144,219,220],{"class":150},"  }\n",[144,222,224],{"class":146,"line":223},8,[144,225,226],{"class":150},"}\n",[19,228,229,230,233,234,237,238,133],{},"Content v3 requires ",[37,231,232],{},"SQLite"," for its database. On Node 22.5+, you can use the native ",[48,235,236],{},"node:sqlite"," module, but for broader compatibility I installed ",[48,239,240],{},"better-sqlite3",[135,242,246],{"className":243,"code":244,"language":245,"meta":140,"style":140},"language-bash shiki shiki-themes github-light github-dark","pnpm add better-sqlite3 --save-dev\n","bash",[48,247,248],{"__ignoreMap":140},[144,249,250,254,257,260],{"class":146,"line":147},[144,251,253],{"class":252},"sScJk","pnpm",[144,255,256],{"class":173}," add",[144,258,259],{"class":173}," better-sqlite3",[144,261,262],{"class":157}," --save-dev\n",[23,264,266],{"id":265},"phase-2-collections-configuration","Phase 2: Collections Configuration",[19,268,269,270,273,274,277,278,133],{},"The biggest conceptual shift in Content v3 is ",[37,271,272],{},"collections",". Instead of ",[48,275,276],{},"queryContent('blog')",", you now define typed collections in ",[48,279,280],{},"content.config.ts",[135,282,286],{"className":283,"code":284,"language":285,"meta":140,"style":140},"language-ts shiki shiki-themes github-light github-dark","import { defineContentConfig, defineCollection, z } from '@nuxt\u002Fcontent'\n\nexport default defineContentConfig({\n  collections: {\n    blog: defineCollection({\n      type: 'page',\n      source: 'blog\u002F*.md',\n      schema: z.object({\n        title: z.string(),\n        description: z.string(),\n        img: z.string().optional(),\n        tags: z.array(z.string()).optional(),\n        author: z\n          .object({\n            name: z.string(),\n            bio: z.string()\n          })\n          .optional()\n      })\n    }),\n    tags: defineCollection({\n      type: 'data',\n      source: 'tags\u002F*.md'\n    }),\n    pages: defineCollection({\n      type: 'page',\n      source: '*.md'\n    })\n  }\n})\n","ts",[48,287,288,303,309,323,328,338,348,358,368,380,390,406,427,433,443,453,464,470,479,485,491,501,511,519,524,534,543,551,557,562],{"__ignoreMap":140},[144,289,290,294,297,300],{"class":146,"line":147},[144,291,293],{"class":292},"szBVR","import",[144,295,296],{"class":150}," { defineContentConfig, defineCollection, z } ",[144,298,299],{"class":292},"from",[144,301,302],{"class":173}," '@nuxt\u002Fcontent'\n",[144,304,305],{"class":146,"line":154},[144,306,308],{"emptyLinePlaceholder":307},true,"\n",[144,310,311,314,317,320],{"class":146,"line":164},[144,312,313],{"class":292},"export",[144,315,316],{"class":292}," default",[144,318,319],{"class":252}," defineContentConfig",[144,321,322],{"class":150},"({\n",[144,324,325],{"class":146,"line":180},[144,326,327],{"class":150},"  collections: {\n",[144,329,330,333,336],{"class":146,"line":193},[144,331,332],{"class":150},"    blog: ",[144,334,335],{"class":252},"defineCollection",[144,337,322],{"class":150},[144,339,340,343,346],{"class":146,"line":206},[144,341,342],{"class":150},"      type: ",[144,344,345],{"class":173},"'page'",[144,347,177],{"class":150},[144,349,350,353,356],{"class":146,"line":217},[144,351,352],{"class":150},"      source: ",[144,354,355],{"class":173},"'blog\u002F*.md'",[144,357,177],{"class":150},[144,359,360,363,366],{"class":146,"line":223},[144,361,362],{"class":150},"      schema: z.",[144,364,365],{"class":252},"object",[144,367,322],{"class":150},[144,369,371,374,377],{"class":146,"line":370},9,[144,372,373],{"class":150},"        title: z.",[144,375,376],{"class":252},"string",[144,378,379],{"class":150},"(),\n",[144,381,383,386,388],{"class":146,"line":382},10,[144,384,385],{"class":150},"        description: z.",[144,387,376],{"class":252},[144,389,379],{"class":150},[144,391,393,396,398,401,404],{"class":146,"line":392},11,[144,394,395],{"class":150},"        img: z.",[144,397,376],{"class":252},[144,399,400],{"class":150},"().",[144,402,403],{"class":252},"optional",[144,405,379],{"class":150},[144,407,409,412,415,418,420,423,425],{"class":146,"line":408},12,[144,410,411],{"class":150},"        tags: z.",[144,413,414],{"class":252},"array",[144,416,417],{"class":150},"(z.",[144,419,376],{"class":252},[144,421,422],{"class":150},"()).",[144,424,403],{"class":252},[144,426,379],{"class":150},[144,428,430],{"class":146,"line":429},13,[144,431,432],{"class":150},"        author: z\n",[144,434,436,439,441],{"class":146,"line":435},14,[144,437,438],{"class":150},"          .",[144,440,365],{"class":252},[144,442,322],{"class":150},[144,444,446,449,451],{"class":146,"line":445},15,[144,447,448],{"class":150},"            name: z.",[144,450,376],{"class":252},[144,452,379],{"class":150},[144,454,456,459,461],{"class":146,"line":455},16,[144,457,458],{"class":150},"            bio: z.",[144,460,376],{"class":252},[144,462,463],{"class":150},"()\n",[144,465,467],{"class":146,"line":466},17,[144,468,469],{"class":150},"          })\n",[144,471,473,475,477],{"class":146,"line":472},18,[144,474,438],{"class":150},[144,476,403],{"class":252},[144,478,463],{"class":150},[144,480,482],{"class":146,"line":481},19,[144,483,484],{"class":150},"      })\n",[144,486,488],{"class":146,"line":487},20,[144,489,490],{"class":150},"    }),\n",[144,492,494,497,499],{"class":146,"line":493},21,[144,495,496],{"class":150},"    tags: ",[144,498,335],{"class":252},[144,500,322],{"class":150},[144,502,504,506,509],{"class":146,"line":503},22,[144,505,342],{"class":150},[144,507,508],{"class":173},"'data'",[144,510,177],{"class":150},[144,512,514,516],{"class":146,"line":513},23,[144,515,352],{"class":150},[144,517,518],{"class":173},"'tags\u002F*.md'\n",[144,520,522],{"class":146,"line":521},24,[144,523,490],{"class":150},[144,525,527,530,532],{"class":146,"line":526},25,[144,528,529],{"class":150},"    pages: ",[144,531,335],{"class":252},[144,533,322],{"class":150},[144,535,537,539,541],{"class":146,"line":536},26,[144,538,342],{"class":150},[144,540,345],{"class":173},[144,542,177],{"class":150},[144,544,546,548],{"class":146,"line":545},27,[144,547,352],{"class":150},[144,549,550],{"class":173},"'*.md'\n",[144,552,554],{"class":146,"line":553},28,[144,555,556],{"class":150},"    })\n",[144,558,560],{"class":146,"line":559},29,[144,561,220],{"class":150},[144,563,565],{"class":146,"line":564},30,[144,566,567],{"class":150},"})\n",[19,569,570],{},"Key differences from v2:",[31,572,573,583,589],{},[34,574,575,578,579,582],{},[48,576,577],{},"type: 'page'"," vs ",[48,580,581],{},"type: 'data'"," — pages get URLs, data files are just structured content",[34,584,585,588],{},[48,586,587],{},"source"," uses glob patterns to define what files belong to a collection",[34,590,591,594,595,598],{},[48,592,593],{},"schema"," is validated with ",[37,596,597],{},"Zod"," at build time",[23,600,602],{"id":601},"phase-3-query-api-changes","Phase 3: Query API Changes",[604,605,607],"h3",{"id":606},"v2-syntax-old","v2 Syntax (Old)",[135,609,611],{"className":283,"code":610,"language":285,"meta":140,"style":140},"const article = await $content('articles', params.slug).fetch()\nconst [prev, next] = await $content('articles')\n  .only(['title', 'slug'])\n  .sortBy('createdAt', 'asc')\n  .surround(params.slug)\n  .fetch()\n",[48,612,613,644,676,698,717,727],{"__ignoreMap":140},[144,614,615,618,621,624,627,630,633,636,639,642],{"class":146,"line":147},[144,616,617],{"class":292},"const",[144,619,620],{"class":157}," article",[144,622,623],{"class":292}," =",[144,625,626],{"class":292}," await",[144,628,629],{"class":252}," $content",[144,631,632],{"class":150},"(",[144,634,635],{"class":173},"'articles'",[144,637,638],{"class":150},", params.slug).",[144,640,641],{"class":252},"fetch",[144,643,463],{"class":150},[144,645,646,648,651,654,656,659,662,665,667,669,671,673],{"class":146,"line":154},[144,647,617],{"class":292},[144,649,650],{"class":150}," [",[144,652,653],{"class":157},"prev",[144,655,100],{"class":150},[144,657,658],{"class":157},"next",[144,660,661],{"class":150},"] ",[144,663,664],{"class":292},"=",[144,666,626],{"class":292},[144,668,629],{"class":252},[144,670,632],{"class":150},[144,672,635],{"class":173},[144,674,675],{"class":150},")\n",[144,677,678,681,684,687,690,692,695],{"class":146,"line":164},[144,679,680],{"class":150},"  .",[144,682,683],{"class":252},"only",[144,685,686],{"class":150},"([",[144,688,689],{"class":173},"'title'",[144,691,100],{"class":150},[144,693,694],{"class":173},"'slug'",[144,696,697],{"class":150},"])\n",[144,699,700,702,705,707,710,712,715],{"class":146,"line":180},[144,701,680],{"class":150},[144,703,704],{"class":252},"sortBy",[144,706,632],{"class":150},[144,708,709],{"class":173},"'createdAt'",[144,711,100],{"class":150},[144,713,714],{"class":173},"'asc'",[144,716,675],{"class":150},[144,718,719,721,724],{"class":146,"line":193},[144,720,680],{"class":150},[144,722,723],{"class":252},"surround",[144,725,726],{"class":150},"(params.slug)\n",[144,728,729,731,733],{"class":146,"line":206},[144,730,680],{"class":150},[144,732,641],{"class":252},[144,734,463],{"class":150},[604,736,738],{"id":737},"v3-syntax-new","v3 Syntax (New)",[135,740,742],{"className":283,"code":741,"language":285,"meta":140,"style":140},"const { data: post } = await useAsyncData(() =>\n  queryCollection('blog')\n    .path('\u002Fblog\u002F' + slug)\n    .first()\n)\n\nconst { data: surround } = await useAsyncData(() =>\n  queryCollectionItemSurroundings('blog', '\u002Fblog\u002F' + slug, {\n    fields: ['title', 'path']\n  })\n)\n",[48,743,744,776,788,807,816,820,824,848,866,881,886],{"__ignoreMap":140},[144,745,746,748,751,755,757,760,763,765,767,770,773],{"class":146,"line":147},[144,747,617],{"class":292},[144,749,750],{"class":150}," { ",[144,752,754],{"class":753},"s4XuR","data",[144,756,170],{"class":150},[144,758,759],{"class":157},"post",[144,761,762],{"class":150}," } ",[144,764,664],{"class":292},[144,766,626],{"class":292},[144,768,769],{"class":252}," useAsyncData",[144,771,772],{"class":150},"(() ",[144,774,775],{"class":292},"=>\n",[144,777,778,781,783,786],{"class":146,"line":154},[144,779,780],{"class":252},"  queryCollection",[144,782,632],{"class":150},[144,784,785],{"class":173},"'blog'",[144,787,675],{"class":150},[144,789,790,793,796,798,801,804],{"class":146,"line":164},[144,791,792],{"class":150},"    .",[144,794,795],{"class":252},"path",[144,797,632],{"class":150},[144,799,800],{"class":173},"'\u002Fblog\u002F'",[144,802,803],{"class":292}," +",[144,805,806],{"class":150}," slug)\n",[144,808,809,811,814],{"class":146,"line":180},[144,810,792],{"class":150},[144,812,813],{"class":252},"first",[144,815,463],{"class":150},[144,817,818],{"class":146,"line":193},[144,819,675],{"class":150},[144,821,822],{"class":146,"line":206},[144,823,308],{"emptyLinePlaceholder":307},[144,825,826,828,830,832,834,836,838,840,842,844,846],{"class":146,"line":217},[144,827,617],{"class":292},[144,829,750],{"class":150},[144,831,754],{"class":753},[144,833,170],{"class":150},[144,835,723],{"class":157},[144,837,762],{"class":150},[144,839,664],{"class":292},[144,841,626],{"class":292},[144,843,769],{"class":252},[144,845,772],{"class":150},[144,847,775],{"class":292},[144,849,850,853,855,857,859,861,863],{"class":146,"line":223},[144,851,852],{"class":252},"  queryCollectionItemSurroundings",[144,854,632],{"class":150},[144,856,785],{"class":173},[144,858,100],{"class":150},[144,860,800],{"class":173},[144,862,803],{"class":292},[144,864,865],{"class":150}," slug, {\n",[144,867,868,871,873,875,878],{"class":146,"line":370},[144,869,870],{"class":150},"    fields: [",[144,872,689],{"class":173},[144,874,100],{"class":150},[144,876,877],{"class":173},"'path'",[144,879,880],{"class":150},"]\n",[144,882,883],{"class":146,"line":382},[144,884,885],{"class":150},"  })\n",[144,887,888],{"class":146,"line":392},[144,889,675],{"class":150},[19,891,892],{},"Major changes:",[31,894,895,904,913,921,930],{},[34,896,897,900,901],{},[48,898,899],{},"queryContent()"," → ",[48,902,903],{},"queryCollection('collectionName')",[34,905,906,900,909,912],{},[48,907,908],{},"_path",[48,910,911],{},".path"," (no underscore)",[34,914,915,900,918],{},[48,916,917],{},"sortBy('createdAt', 'asc')",[48,919,920],{},".order('createdAt', 'ASC')",[34,922,923,900,926,929],{},[48,924,925],{},"surround()",[48,927,928],{},"queryCollectionItemSurroundings()"," (separate composable!)",[34,931,932,900,935,938,939],{},[48,933,934],{},"fetch()",[48,936,937],{},".all()"," or ",[48,940,941],{},".first()",[23,943,945],{"id":944},"phase-4-component-changes","Phase 4: Component Changes",[604,947,949,952],{"id":948},"contentdoc-is-gone",[48,950,951],{},"\u003CContentDoc>"," is Gone",[19,954,955,956,133],{},"In v3, all content is rendered with ",[48,957,958],{},"\u003CContentRenderer>",[135,960,964],{"className":961,"code":962,"language":963,"meta":140,"style":140},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003CContentRenderer v-if=\"page\" :value=\"page\" \u002F>\n\u003C\u002Ftemplate>\n","vue",[48,965,966,978,1004],{"__ignoreMap":140},[144,967,968,971,975],{"class":146,"line":147},[144,969,970],{"class":150},"\u003C",[144,972,974],{"class":973},"s9eBZ","template",[144,976,977],{"class":150},">\n",[144,979,980,983,986,989,991,994,997,999,1001],{"class":146,"line":154},[144,981,982],{"class":150},"  \u003C",[144,984,985],{"class":973},"ContentRenderer",[144,987,988],{"class":252}," v-if",[144,990,664],{"class":150},[144,992,993],{"class":173},"\"page\"",[144,995,996],{"class":252}," :value",[144,998,664],{"class":150},[144,1000,993],{"class":173},[144,1002,1003],{"class":150}," \u002F>\n",[144,1005,1006,1009,1011],{"class":146,"line":164},[144,1007,1008],{"class":150},"\u003C\u002F",[144,1010,974],{"class":973},[144,1012,977],{"class":150},[19,1014,1015,1016,100,1018,100,1021,1024,1025,1028,1029,1032],{},"The ",[48,1017,951],{},[48,1019,1020],{},"\u003CContentList>",[48,1022,1023],{},"\u003CContentQuery>"," and ",[48,1026,1027],{},"\u003CContentNavigation>"," components from v2 are ",[37,1030,1031],{},"removed",".",[604,1034,1036],{"id":1035},"document-driven-mode","Document Driven Mode",[19,1038,1039],{},"The automatic page generation from markdown files is gone. You now need explicit pages:",[135,1041,1043],{"className":961,"code":1042,"language":963,"meta":140,"style":140},"\u003C!-- pages\u002F[...slug].vue -->\n\u003Cscript setup>\nconst route = useRoute()\nconst { data: page } = await useAsyncData(route.path, () =>\n  queryCollection('pages').path(route.path).first()\n)\n\u003C\u002Fscript>\n\u003Ctemplate>\n  \u003CContentRenderer v-if=\"page\" :value=\"page\" \u002F>\n\u003C\u002Ftemplate>\n",[48,1044,1045,1051,1063,1077,1103,1124,1128,1136,1144,1164],{"__ignoreMap":140},[144,1046,1047],{"class":146,"line":147},[144,1048,1050],{"class":1049},"sJ8bj","\u003C!-- pages\u002F[...slug].vue -->\n",[144,1052,1053,1055,1058,1061],{"class":146,"line":154},[144,1054,970],{"class":150},[144,1056,1057],{"class":973},"script",[144,1059,1060],{"class":252}," setup",[144,1062,977],{"class":150},[144,1064,1065,1067,1070,1072,1075],{"class":146,"line":164},[144,1066,617],{"class":292},[144,1068,1069],{"class":157}," route",[144,1071,623],{"class":292},[144,1073,1074],{"class":252}," useRoute",[144,1076,463],{"class":150},[144,1078,1079,1081,1083,1085,1087,1090,1092,1094,1096,1098,1101],{"class":146,"line":180},[144,1080,617],{"class":292},[144,1082,750],{"class":150},[144,1084,754],{"class":753},[144,1086,170],{"class":150},[144,1088,1089],{"class":157},"page",[144,1091,762],{"class":150},[144,1093,664],{"class":292},[144,1095,626],{"class":292},[144,1097,769],{"class":252},[144,1099,1100],{"class":150},"(route.path, () ",[144,1102,775],{"class":292},[144,1104,1105,1107,1109,1112,1115,1117,1120,1122],{"class":146,"line":193},[144,1106,780],{"class":252},[144,1108,632],{"class":150},[144,1110,1111],{"class":173},"'pages'",[144,1113,1114],{"class":150},").",[144,1116,795],{"class":252},[144,1118,1119],{"class":150},"(route.path).",[144,1121,813],{"class":252},[144,1123,463],{"class":150},[144,1125,1126],{"class":146,"line":206},[144,1127,675],{"class":150},[144,1129,1130,1132,1134],{"class":146,"line":217},[144,1131,1008],{"class":150},[144,1133,1057],{"class":973},[144,1135,977],{"class":150},[144,1137,1138,1140,1142],{"class":146,"line":223},[144,1139,970],{"class":150},[144,1141,974],{"class":973},[144,1143,977],{"class":150},[144,1145,1146,1148,1150,1152,1154,1156,1158,1160,1162],{"class":146,"line":370},[144,1147,982],{"class":150},[144,1149,985],{"class":973},[144,1151,988],{"class":252},[144,1153,664],{"class":150},[144,1155,993],{"class":173},[144,1157,996],{"class":252},[144,1159,664],{"class":150},[144,1161,993],{"class":173},[144,1163,1003],{"class":150},[144,1165,1166,1168,1170],{"class":146,"line":382},[144,1167,1008],{"class":150},[144,1169,974],{"class":973},[144,1171,977],{"class":150},[19,1173,1174],{},"This gives you full control over layouts and error handling.",[23,1176,1178],{"id":1177},"phase-5-routing-changes","Phase 5: Routing Changes",[19,1180,1181],{},"Nuxt 3 uses bracket syntax for dynamic routes:",[1183,1184,1185,1198],"table",{},[1186,1187,1188],"thead",{},[1189,1190,1191,1195],"tr",{},[1192,1193,1194],"th",{},"Old (Nuxt 2)",[1192,1196,1197],{},"New (Nuxt 3)",[1199,1200,1201,1214,1226],"tbody",{},[1189,1202,1203,1209],{},[1204,1205,1206],"td",{},[48,1207,1208],{},"pages\u002Fblog\u002F_slug.vue",[1204,1210,1211],{},[48,1212,1213],{},"pages\u002Fblog\u002F[slug].vue",[1189,1215,1216,1221],{},[1204,1217,1218],{},[48,1219,1220],{},"pages\u002Fblog\u002Ftag\u002F_tag.vue",[1204,1222,1223],{},[48,1224,1225],{},"pages\u002Fblog\u002Ftag\u002F[tag].vue",[1189,1227,1228,1233],{},[1204,1229,1230],{},[48,1231,1232],{},"pages\u002Fblog\u002Fauthor\u002F_author.vue",[1204,1234,1235],{},[48,1236,1237],{},"pages\u002Fblog\u002Fauthor\u002F[author].vue",[19,1239,1240,1241,1244],{},"And the ",[48,1242,1243],{},"navigateTo()"," composable replaces raw redirects:",[135,1246,1248],{"className":961,"code":1247,"language":963,"meta":140,"style":140},"\u003Cscript setup>\nnavigateTo('\u002Finstagram', { replace: true })\n\u003C\u002Fscript>\n",[48,1249,1250,1260,1279],{"__ignoreMap":140},[144,1251,1252,1254,1256,1258],{"class":146,"line":147},[144,1253,970],{"class":150},[144,1255,1057],{"class":973},[144,1257,1060],{"class":252},[144,1259,977],{"class":150},[144,1261,1262,1265,1267,1270,1273,1276],{"class":146,"line":154},[144,1263,1264],{"class":252},"navigateTo",[144,1266,632],{"class":150},[144,1268,1269],{"class":173},"'\u002Finstagram'",[144,1271,1272],{"class":150},", { replace: ",[144,1274,1275],{"class":157},"true",[144,1277,1278],{"class":150}," })\n",[144,1280,1281,1283,1285],{"class":146,"line":164},[144,1282,1008],{"class":150},[144,1284,1057],{"class":973},[144,1286,977],{"class":150},[23,1288,1290],{"id":1289},"phase-6-styling-goodbye-tailwind","Phase 6: Styling — Goodbye Tailwind",[19,1292,1293,1294,1297],{},"I chose to ",[37,1295,1296],{},"remove Tailwind entirely"," and switch to hand-written SCSS. This was a deliberate design decision to:",[1299,1300,1301,1304,1307],"ol",{},[34,1302,1303],{},"Reduce dependency footprint",[34,1305,1306],{},"Have full control over the CSS output",[34,1308,1309],{},"Use semantic class names instead of utility soup",[19,1311,1312,1313,133],{},"All components now use ",[48,1314,1315],{},"\u003Cstyle scoped lang=\"scss\">",[135,1317,1321],{"className":1318,"code":1319,"language":1320,"meta":140,"style":140},"language-scss shiki shiki-themes github-light github-dark",".article-card {\n  display: flex;\n  gap: 1rem;\n  padding: 1rem;\n  border: 1px solid rgba(255, 255, 255, 0.1);\n\n  &:hover {\n    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2);\n  }\n}\n","scss",[48,1322,1323,1331,1344,1359,1372,1411,1415,1425,1466,1470],{"__ignoreMap":140},[144,1324,1325,1328],{"class":146,"line":147},[144,1326,1327],{"class":252},".article-card",[144,1329,1330],{"class":150}," {\n",[144,1332,1333,1336,1338,1341],{"class":146,"line":154},[144,1334,1335],{"class":157},"  display",[144,1337,170],{"class":150},[144,1339,1340],{"class":157},"flex",[144,1342,1343],{"class":150},";\n",[144,1345,1346,1349,1351,1354,1357],{"class":146,"line":164},[144,1347,1348],{"class":157},"  gap",[144,1350,170],{"class":150},[144,1352,1353],{"class":157},"1",[144,1355,1356],{"class":292},"rem",[144,1358,1343],{"class":150},[144,1360,1361,1364,1366,1368,1370],{"class":146,"line":180},[144,1362,1363],{"class":157},"  padding",[144,1365,170],{"class":150},[144,1367,1353],{"class":157},[144,1369,1356],{"class":292},[144,1371,1343],{"class":150},[144,1373,1374,1377,1379,1381,1384,1387,1390,1392,1395,1397,1399,1401,1403,1405,1408],{"class":146,"line":193},[144,1375,1376],{"class":157},"  border",[144,1378,170],{"class":150},[144,1380,1353],{"class":157},[144,1382,1383],{"class":292},"px",[144,1385,1386],{"class":157}," solid",[144,1388,1389],{"class":157}," rgba",[144,1391,632],{"class":150},[144,1393,1394],{"class":157},"255",[144,1396,100],{"class":150},[144,1398,1394],{"class":157},[144,1400,100],{"class":150},[144,1402,1394],{"class":157},[144,1404,100],{"class":150},[144,1406,1407],{"class":157},"0.1",[144,1409,1410],{"class":150},");\n",[144,1412,1413],{"class":146,"line":206},[144,1414,308],{"emptyLinePlaceholder":307},[144,1416,1417,1420,1423],{"class":146,"line":217},[144,1418,1419],{"class":973},"  &",[144,1421,1422],{"class":252},":hover",[144,1424,1330],{"class":150},[144,1426,1427,1430,1432,1435,1438,1440,1443,1445,1447,1449,1451,1453,1455,1457,1459,1461,1464],{"class":146,"line":223},[144,1428,1429],{"class":157},"    box-shadow",[144,1431,170],{"class":150},[144,1433,1434],{"class":157},"0",[144,1436,1437],{"class":157}," 0",[144,1439,1437],{"class":157},[144,1441,1442],{"class":157}," 1",[144,1444,1383],{"class":292},[144,1446,1389],{"class":157},[144,1448,632],{"class":150},[144,1450,1394],{"class":157},[144,1452,100],{"class":150},[144,1454,1394],{"class":157},[144,1456,100],{"class":150},[144,1458,1394],{"class":157},[144,1460,100],{"class":150},[144,1462,1463],{"class":157},"0.2",[144,1465,1410],{"class":150},[144,1467,1468],{"class":146,"line":370},[144,1469,220],{"class":150},[144,1471,1472],{"class":146,"line":382},[144,1473,226],{"class":150},[19,1475,1476,1477,1480],{},"CSS custom properties in ",[48,1478,1479],{},"main.scss"," handle the theme:",[135,1482,1484],{"className":1318,"code":1483,"language":1320,"meta":140,"style":140},"html {\n  --color-black: #070707;\n  --color-white: #f7f7f7;\n  --max-content-width: 900px;\n}\n",[48,1485,1486,1493,1505,1517,1531],{"__ignoreMap":140},[144,1487,1488,1491],{"class":146,"line":147},[144,1489,1490],{"class":973},"html",[144,1492,1330],{"class":150},[144,1494,1495,1498,1500,1503],{"class":146,"line":154},[144,1496,1497],{"class":753},"  --color-black",[144,1499,170],{"class":150},[144,1501,1502],{"class":157},"#070707",[144,1504,1343],{"class":150},[144,1506,1507,1510,1512,1515],{"class":146,"line":164},[144,1508,1509],{"class":753},"  --color-white",[144,1511,170],{"class":150},[144,1513,1514],{"class":157},"#f7f7f7",[144,1516,1343],{"class":150},[144,1518,1519,1522,1524,1527,1529],{"class":146,"line":180},[144,1520,1521],{"class":753},"  --max-content-width",[144,1523,170],{"class":150},[144,1525,1526],{"class":157},"900",[144,1528,1383],{"class":292},[144,1530,1343],{"class":150},[144,1532,1533],{"class":146,"line":193},[144,1534,226],{"class":150},[23,1536,1538],{"id":1537},"phase-7-what-broke-and-how-i-fixed-it","Phase 7: What Broke and How I Fixed It",[604,1540,1542],{"id":1541},"component-auto-registration","Component Auto-Registration",[19,1544,1545,1546,1549,1550,1555],{},"In v2, ",[48,1547,1548],{},"components\u002Fcontent\u002F"," was automatically global for markdown rendering. In v3, these components are still auto-registered for markdown ",[37,1551,1552,1553],{},"within ",[48,1554,958],{},", but if you use them outside markdown you must register them manually.",[19,1557,1558,1561,1562,1024,1564,1566],{},[37,1559,1560],{},"Fix",": My ",[48,1563,103],{},[48,1565,99],{}," are only used inside markdown, so no changes needed.",[604,1568,1570,1572,1573],{"id":1569},"_path-renamed-to-path",[48,1571,908],{}," Renamed to ",[48,1574,795],{},[19,1576,1577,1578,1581,1582,1032],{},"All internal fields lost their underscore prefix. Any code referencing ",[48,1579,1580],{},"post._path"," must change to ",[48,1583,1584],{},"post.path",[604,1586,1588,1591],{"id":1587},"searchcontent-dropped",[48,1589,1590],{},"searchContent()"," Dropped",[19,1593,1594,1595,133],{},"The old search API is gone. In v3, use ",[48,1596,1597],{},"queryCollectionSearchSections()",[135,1599,1601],{"className":283,"code":1600,"language":285,"meta":140,"style":140},"const results = await queryCollectionSearchSections('blog', {\n  ignoredTags: []\n})\n",[48,1602,1603,1624,1629],{"__ignoreMap":140},[144,1604,1605,1607,1610,1612,1614,1617,1619,1621],{"class":146,"line":147},[144,1606,617],{"class":292},[144,1608,1609],{"class":157}," results",[144,1611,623],{"class":292},[144,1613,626],{"class":292},[144,1615,1616],{"class":252}," queryCollectionSearchSections",[144,1618,632],{"class":150},[144,1620,785],{"class":173},[144,1622,1623],{"class":150},", {\n",[144,1625,1626],{"class":146,"line":154},[144,1627,1628],{"class":150},"  ignoredTags: []\n",[144,1630,1631],{"class":146,"line":164},[144,1632,567],{"class":150},[19,1634,1635],{},"I decided to defer live search to a later phase.",[23,1637,1639],{"id":1638},"file-structure-after-migration","File Structure After Migration",[135,1641,1646],{"className":1642,"code":1644,"language":1645},[1643],"language-text","├── components\u002F\n│   ├── content\u002F          # Markdown components (TextWave, MadjozoAnimation, ProseH1)\n│   ├── global\u002F           # Global components (PrevNext, VideoLoop, MadjozoCanvas)\n│   ├── shell\u002F            # Header & Footer\n│   └── ...               # Shared UI (Author, AppSearchInput)\n├── content\u002F\n│   ├── blog\u002F             # Blog posts\n│   ├── tags\u002F             # Tag metadata\n│   ├── about.md          # About page\n│   └── instagram.md      # Links page\n├── pages\u002F\n│   ├── index.vue         # Article list\n│   ├── i.vue             # Redirect to \u002Finstagram\n│   ├── [...slug].vue     # Static pages catch-all\n│   ├── blog\u002F\n│   │   ├── [slug].vue    # Article detail\n│   │   ├── author\u002F\n│   │   │   └── [author].vue\n│   │   └── tag\u002F\n│   │       └── [tag].vue\n├── content.config.ts     # Collection definitions\n└── nuxt.config.ts        # Module config\n","text",[48,1647,1644],{"__ignoreMap":140},[23,1649,1651],{"id":1650},"results","Results",[19,1653,1654],{},"The migration is complete. The site is now:",[31,1656,1657,1663,1669,1675],{},[34,1658,1659,1662],{},[37,1660,1661],{},"Faster",": SQL-backed content queries + Nitro server engine",[34,1664,1665,1668],{},[37,1666,1667],{},"More maintainable",": Type-safe collections + explicit routing",[34,1670,1671,1674],{},[37,1672,1673],{},"Lighter",": No Tailwind, smaller bundle",[34,1676,1677,1680],{},[37,1678,1679],{},"Future-proof",": On the latest Nuxt 3 and Content 3 LTS track",[23,1682,1684],{"id":1683},"resources","Resources",[31,1686,1687,1696,1703,1710],{},[34,1688,1689],{},[1690,1691,1695],"a",{"href":1692,"rel":1693},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fmigration\u002Foverview",[1694],"nofollow","Nuxt 3 Migration Guide",[34,1697,1698],{},[1690,1699,1702],{"href":1700,"rel":1701},"https:\u002F\u002Fcontent.nuxtjs.org\u002F",[1694],"Nuxt Content v3 Docs",[34,1704,1705],{},[1690,1706,1709],{"href":1707,"rel":1708},"https:\u002F\u002Fcontent.nuxtjs.org\u002Fdocs\u002Fcollections\u002Fdefine",[1694],"Collections Guide",[34,1711,1712],{},[1690,1713,1716],{"href":1714,"rel":1715},"https:\u002F\u002Fcontent.nuxtjs.org\u002Fdocs\u002Futils\u002Fquery-collection",[1694],"Query API Reference",[1718,1719],"hr",{},[19,1721,1722],{},[1723,1724,1725],"em",{},"This post was written as part of the migration itself — meta, I know.",[1727,1728,1729],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":140,"searchDepth":154,"depth":154,"links":1731},[1732,1733,1734,1735,1736,1740,1745,1746,1747,1754,1755,1756],{"id":25,"depth":154,"text":26},{"id":69,"depth":154,"text":70},{"id":125,"depth":154,"text":126},{"id":265,"depth":154,"text":266},{"id":601,"depth":154,"text":602,"children":1737},[1738,1739],{"id":606,"depth":164,"text":607},{"id":737,"depth":164,"text":738},{"id":944,"depth":154,"text":945,"children":1741},[1742,1744],{"id":948,"depth":164,"text":1743},"\u003CContentDoc> is Gone",{"id":1035,"depth":164,"text":1036},{"id":1177,"depth":154,"text":1178},{"id":1289,"depth":154,"text":1290},{"id":1537,"depth":154,"text":1538,"children":1748},[1749,1750,1752],{"id":1541,"depth":164,"text":1542},{"id":1569,"depth":164,"text":1751},"_path Renamed to path",{"id":1587,"depth":164,"text":1753},"searchContent() Dropped",{"id":1638,"depth":154,"text":1639},{"id":1650,"depth":154,"text":1651},{"id":1683,"depth":154,"text":1684},"2026-06-16","A deep dive into migrating my personal site from Nuxt 2 + Content v2 to Nuxt 3.21 + Content 3.14","md",{},"\u002Fblog\u002Fnuxt3-content3-migration",{"title":6,"description":1758},"blog\u002Fnuxt3-content3-migration",[1765,1766,1767,963],"nuxt","web development","migration","2026-06-17","bxbwaFpuekX4jMFiGi4x8g23Ur7uDt7q11Xfj63YVGY",{"id":1771,"title":1772,"alt":1773,"author":1774,"body":1775,"date":1851,"description":1852,"extension":1759,"img":1853,"meta":1854,"navigation":307,"path":1855,"seo":1856,"stem":1857,"tags":1858,"updated":1860,"__hash__":1861},"blog\u002Fblog\u002Fflow-field-exploration.md","Flow Field Exploration","flow field",{"name":9,"bio":10},{"type":12,"value":1776,"toc":1849},[1777,1780,1785,1788],[19,1778,1779],{},"I will probably change the media with better quality later, hope you like it.",[19,1781,1784],{"className":1782},[1783],"text-gray-600","\n#javascript #canvas2d\n",[1727,1786,1787],{},"\n  .flow-field-preview {\n    display: flex;\n    flex-wrap: wrap;\n    width: 100%;\n    align-items: center;\n    justify-content: center;\n  }\n  .flow-field-preview video,\n  .flow-field-preview .video-loop__fallback-ios {\n    \u002F* background: rgba(64, 64, 64, 1); *\u002F\n    max-width: 400px;\n    width: 32%;\n    border: 1px solid black;\n    aspect-ratio: 1 \u002F 1;\n  }\n  @media screen and (max-width: 500px) {\n    .flow-field-preview {\n      flex-direction: column;\n    }\n    .flow-field-preview video,\n    .flow-field-preview .video-loop__fallback-ios {\n      width: 100%;\n    }\n  }\n",[1789,1790,1793,1794,1793,1798,1793,1801,1793,1804,1793,1807,1793,1810,1793,1813,1793,1816,1793,1819,1793,1822,1793,1825,1793,1828,1793,1831,1793,1834,1793,1837,1793,1840,1793,1843,1793,1846],"div",{"className":1791},[1792],"flow-field-preview","\n  ",[1795,1796],"video-loop",{"src":1797},"\u002F20220221\u002Fsmall01",[1795,1799],{"src":1800},"\u002F20220221\u002Fsmall02",[1795,1802],{"src":1803},"\u002F20220221\u002Fsmall03",[1795,1805],{"src":1806},"\u002F20220221\u002Fsmall04",[1795,1808],{"src":1809},"\u002F20220221\u002Fsmall05",[1795,1811],{"src":1812},"\u002F20220221\u002Fsmall06",[1795,1814],{"src":1815},"\u002F20220221\u002Fsmall07",[1795,1817],{"src":1818},"\u002F20220221\u002Fsmall08",[1795,1820],{"src":1821},"\u002F20220221\u002Fsmall09",[1795,1823],{"src":1824},"\u002F20220221\u002Fsmall10",[1795,1826],{"src":1827},"\u002F20220221\u002Fsmall11",[1795,1829],{"src":1830},"\u002F20220221\u002Fsmall12",[1795,1832],{"src":1833},"\u002F20220221\u002Fsmall14",[1795,1835],{"src":1836},"\u002F20220221\u002Fsmall15",[1795,1838],{"src":1839},"\u002F20220221\u002Fsmall16",[1795,1841],{"src":1842},"\u002F20220221\u002Fsmall13",[1795,1844],{"src":1845},"\u002F20220221\u002Fsmall17",[1795,1847],{"src":1848},"\u002F20220221\u002Fsmall18",{"title":140,"searchDepth":154,"depth":154,"links":1850},[],"2022-02-21","Some Flow Field exploration I want to share","\u002F20220221\u002Fcover.png",{},"\u002Fblog\u002Fflow-field-exploration",{"title":1772,"description":1852},"blog\u002Fflow-field-exploration",[1859,1766],"canvas 2d","2023-07-06","--8pKhd4GQ_0uj7yIGHEjjq-R9w6FY_DoN6WBYrlLRU",1781661355104]