[{"data":1,"prerenderedAt":967},["ShallowReactive",2],{"post-nuxt-content-renewal":3},{"id":4,"title":5,"body":6,"date":959,"description":960,"extension":961,"meta":962,"navigation":145,"path":963,"seo":964,"stem":965,"__hash__":966},"blog\u002Fblog\u002Fnuxt-content-renewal.md","個人ブログを Nuxt Content でリニューアルした話",{"type":7,"value":8,"toc":946},"minimark",[9,13,17,20,23,29,51,56,64,69,77,81,85,96,101,250,255,381,384,387,397,462,465,468,475,482,635,639,642,649,702,709,750,754,768,819,822,825,828,835,916,919,922,939,942],[10,11,12],"h2",{"id":12},"はじめに",[14,15,16],"p",{},"これまで Astro 5 で構築していた個人ブログを、Nuxt Content v3 を中心とした構成にリニューアルしました。",[14,18,19],{},"Astro は素晴らしいフレームワークですが、Vue エコシステムが好きな自分にとって、Vue コンポーネントを自然に書ける環境に移行したいと思っていました。",[10,21,22],{"id":22},"新しい技術スタック",[14,24,25],{},[26,27,28],"strong",{},"コア",[30,31,32,39,45],"ul",{},[33,34,35,38],"li",{},[26,36,37],{},"Nuxt 4"," - Vue フルスタックフレームワーク",[33,40,41,44],{},[26,42,43],{},"Nuxt Content v3"," - SQLite ベースのコンテンツ管理",[33,46,47,50],{},[26,48,49],{},"Vite+"," - Vite \u002F Rolldown \u002F Vitest \u002F Oxlint を統合したツールチェーン",[14,52,53],{},[26,54,55],{},"スタイリング",[30,57,58],{},[33,59,60,63],{},[26,61,62],{},"UnoCSS"," - Tailwind 互換のオンデマンド CSS エンジン",[14,65,66],{},[26,67,68],{},"デプロイ",[30,70,71],{},[33,72,73,76],{},[26,74,75],{},"Cloudflare Workers"," - エッジでの SSR \u002F 静的配信",[10,78,80],{"id":79},"astro-から-nuxt-content-v3-への移行","Astro から Nuxt Content v3 への移行",[82,83,84],"h3",{"id":84},"コンテンツコレクションの違い",[14,86,87,88,92,93,95],{},"Astro の ",[89,90,91],"code",{},"defineCollection"," と Nuxt Content v3 の ",[89,94,91],{}," は似ていますが、スキーマ定義と取得 API が異なります。",[14,97,98],{},[26,99,100],{},"Astro の場合",[102,103,108],"pre",{"className":104,"code":105,"language":106,"meta":107,"style":107},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F src\u002Fcontent\u002Fconfig.ts\nimport { defineCollection, z } from 'astro:content';\n\nconst blogCollection = defineCollection({\n  type: 'content',\n  schema: z.object({\n    title: z.string(),\n    date: z.string().transform((str) => new Date(str)),\n  }),\n});\n","typescript","",[89,109,110,119,140,147,167,179,190,202,238,244],{"__ignoreMap":107},[111,112,115],"span",{"class":113,"line":114},"line",1,[111,116,118],{"class":117},"sJ8bj","\u002F\u002F src\u002Fcontent\u002Fconfig.ts\n",[111,120,122,126,130,133,137],{"class":113,"line":121},2,[111,123,125],{"class":124},"szBVR","import",[111,127,129],{"class":128},"sVt8B"," { defineCollection, z } ",[111,131,132],{"class":124},"from",[111,134,136],{"class":135},"sZZnC"," 'astro:content'",[111,138,139],{"class":128},";\n",[111,141,143],{"class":113,"line":142},3,[111,144,146],{"emptyLinePlaceholder":145},true,"\n",[111,148,150,153,157,160,164],{"class":113,"line":149},4,[111,151,152],{"class":124},"const",[111,154,156],{"class":155},"sj4cs"," blogCollection",[111,158,159],{"class":124}," =",[111,161,163],{"class":162},"sScJk"," defineCollection",[111,165,166],{"class":128},"({\n",[111,168,170,173,176],{"class":113,"line":169},5,[111,171,172],{"class":128},"  type: ",[111,174,175],{"class":135},"'content'",[111,177,178],{"class":128},",\n",[111,180,182,185,188],{"class":113,"line":181},6,[111,183,184],{"class":128},"  schema: z.",[111,186,187],{"class":162},"object",[111,189,166],{"class":128},[111,191,193,196,199],{"class":113,"line":192},7,[111,194,195],{"class":128},"    title: z.",[111,197,198],{"class":162},"string",[111,200,201],{"class":128},"(),\n",[111,203,205,208,210,213,216,219,223,226,229,232,235],{"class":113,"line":204},8,[111,206,207],{"class":128},"    date: z.",[111,209,198],{"class":162},[111,211,212],{"class":128},"().",[111,214,215],{"class":162},"transform",[111,217,218],{"class":128},"((",[111,220,222],{"class":221},"s4XuR","str",[111,224,225],{"class":128},") ",[111,227,228],{"class":124},"=>",[111,230,231],{"class":124}," new",[111,233,234],{"class":162}," Date",[111,236,237],{"class":128},"(str)),\n",[111,239,241],{"class":113,"line":240},9,[111,242,243],{"class":128},"  }),\n",[111,245,247],{"class":113,"line":246},10,[111,248,249],{"class":128},"});\n",[14,251,252],{},[26,253,254],{},"Nuxt Content v3 の場合",[102,256,258],{"className":104,"code":257,"language":106,"meta":107,"style":107},"\u002F\u002F content.config.ts\nimport { defineContentConfig, defineCollection, z } from '@nuxt\u002Fcontent';\n\nexport default defineContentConfig({\n  collections: {\n    blog: defineCollection({\n      type: 'page',\n      source: 'blog\u002F**',\n      schema: z.object({\n        title: z.string(),\n        date: z.string(),\n      }),\n    }),\n  },\n});\n",[89,259,260,265,279,283,296,301,310,320,330,339,348,358,364,370,376],{"__ignoreMap":107},[111,261,262],{"class":113,"line":114},[111,263,264],{"class":117},"\u002F\u002F content.config.ts\n",[111,266,267,269,272,274,277],{"class":113,"line":121},[111,268,125],{"class":124},[111,270,271],{"class":128}," { defineContentConfig, defineCollection, z } ",[111,273,132],{"class":124},[111,275,276],{"class":135}," '@nuxt\u002Fcontent'",[111,278,139],{"class":128},[111,280,281],{"class":113,"line":142},[111,282,146],{"emptyLinePlaceholder":145},[111,284,285,288,291,294],{"class":113,"line":149},[111,286,287],{"class":124},"export",[111,289,290],{"class":124}," default",[111,292,293],{"class":162}," defineContentConfig",[111,295,166],{"class":128},[111,297,298],{"class":113,"line":169},[111,299,300],{"class":128},"  collections: {\n",[111,302,303,306,308],{"class":113,"line":181},[111,304,305],{"class":128},"    blog: ",[111,307,91],{"class":162},[111,309,166],{"class":128},[111,311,312,315,318],{"class":113,"line":192},[111,313,314],{"class":128},"      type: ",[111,316,317],{"class":135},"'page'",[111,319,178],{"class":128},[111,321,322,325,328],{"class":113,"line":204},[111,323,324],{"class":128},"      source: ",[111,326,327],{"class":135},"'blog\u002F**'",[111,329,178],{"class":128},[111,331,332,335,337],{"class":113,"line":240},[111,333,334],{"class":128},"      schema: z.",[111,336,187],{"class":162},[111,338,166],{"class":128},[111,340,341,344,346],{"class":113,"line":246},[111,342,343],{"class":128},"        title: z.",[111,345,198],{"class":162},[111,347,201],{"class":128},[111,349,351,354,356],{"class":113,"line":350},11,[111,352,353],{"class":128},"        date: z.",[111,355,198],{"class":162},[111,357,201],{"class":128},[111,359,361],{"class":113,"line":360},12,[111,362,363],{"class":128},"      }),\n",[111,365,367],{"class":113,"line":366},13,[111,368,369],{"class":128},"    }),\n",[111,371,373],{"class":113,"line":372},14,[111,374,375],{"class":128},"  },\n",[111,377,379],{"class":113,"line":378},15,[111,380,249],{"class":128},[14,382,383],{},"Nuxt Content v3 はコレクションを SQLite に保存します。ビルド時にコンテンツを処理してデータベースを構築し、クエリを高速化しています。",[82,385,386],{"id":386},"コンテンツの取得",[14,388,87,389,392,393,396],{},[89,390,391],{},"getCollection"," に対応するのが、Nuxt Content v3 の ",[89,394,395],{},"queryCollection"," です。",[102,398,400],{"className":104,"code":399,"language":106,"meta":107,"style":107},"\u002F\u002F Nuxt Content v3\nconst posts = await queryCollection('blog')\n  .order('date', 'DESC')\n  .all();\n",[89,401,402,407,431,452],{"__ignoreMap":107},[111,403,404],{"class":113,"line":114},[111,405,406],{"class":117},"\u002F\u002F Nuxt Content v3\n",[111,408,409,411,414,416,419,422,425,428],{"class":113,"line":121},[111,410,152],{"class":124},[111,412,413],{"class":155}," posts",[111,415,159],{"class":124},[111,417,418],{"class":124}," await",[111,420,421],{"class":162}," queryCollection",[111,423,424],{"class":128},"(",[111,426,427],{"class":135},"'blog'",[111,429,430],{"class":128},")\n",[111,432,433,436,439,441,444,447,450],{"class":113,"line":142},[111,434,435],{"class":128},"  .",[111,437,438],{"class":162},"order",[111,440,424],{"class":128},[111,442,443],{"class":135},"'date'",[111,445,446],{"class":128},", ",[111,448,449],{"class":135},"'DESC'",[111,451,430],{"class":128},[111,453,454,456,459],{"class":113,"line":149},[111,455,435],{"class":128},[111,457,458],{"class":162},"all",[111,460,461],{"class":128},"();\n",[14,463,464],{},"Vue ページ内では auto-import されるため、import 文が不要です。",[82,466,467],{"id":467},"ルーティング",[14,469,470,471,474],{},"Astro のファイルベースルーティングと同様に、Nuxt も ",[89,472,473],{},"pages\u002F"," ディレクトリでルーティングを管理します。",[14,476,477,478,481],{},"ブログ記事の詳細ページは ",[89,479,480],{},"pages\u002Fposts\u002F[...slug].vue"," として定義し、パスからスラッグを取得してコンテンツをクエリしています。",[102,483,485],{"className":104,"code":484,"language":106,"meta":107,"style":107},"\u002F\u002F app\u002Fpages\u002Fposts\u002F[...slug].vue\nconst route = useRoute();\nconst slug = Array.isArray(route.params.slug)\n  ? route.params.slug.join('\u002F')\n  : route.params.slug;\n\nconst { data: post } = await useAsyncData(`post-${slug}`, () =>\n  queryCollection('blog').path(`\u002Fblog\u002F${slug}`).first()\n);\n",[89,486,487,492,506,524,542,550,554,598,630],{"__ignoreMap":107},[111,488,489],{"class":113,"line":114},[111,490,491],{"class":117},"\u002F\u002F app\u002Fpages\u002Fposts\u002F[...slug].vue\n",[111,493,494,496,499,501,504],{"class":113,"line":121},[111,495,152],{"class":124},[111,497,498],{"class":155}," route",[111,500,159],{"class":124},[111,502,503],{"class":162}," useRoute",[111,505,461],{"class":128},[111,507,508,510,513,515,518,521],{"class":113,"line":142},[111,509,152],{"class":124},[111,511,512],{"class":155}," slug",[111,514,159],{"class":124},[111,516,517],{"class":128}," Array.",[111,519,520],{"class":162},"isArray",[111,522,523],{"class":128},"(route.params.slug)\n",[111,525,526,529,532,535,537,540],{"class":113,"line":149},[111,527,528],{"class":124},"  ?",[111,530,531],{"class":128}," route.params.slug.",[111,533,534],{"class":162},"join",[111,536,424],{"class":128},[111,538,539],{"class":135},"'\u002F'",[111,541,430],{"class":128},[111,543,544,547],{"class":113,"line":169},[111,545,546],{"class":124},"  :",[111,548,549],{"class":128}," route.params.slug;\n",[111,551,552],{"class":113,"line":181},[111,553,146],{"emptyLinePlaceholder":145},[111,555,556,558,561,564,567,570,573,576,578,581,583,586,589,592,595],{"class":113,"line":192},[111,557,152],{"class":124},[111,559,560],{"class":128}," { ",[111,562,563],{"class":221},"data",[111,565,566],{"class":128},": ",[111,568,569],{"class":155},"post",[111,571,572],{"class":128}," } ",[111,574,575],{"class":124},"=",[111,577,418],{"class":124},[111,579,580],{"class":162}," useAsyncData",[111,582,424],{"class":128},[111,584,585],{"class":135},"`post-${",[111,587,588],{"class":128},"slug",[111,590,591],{"class":135},"}`",[111,593,594],{"class":128},", () ",[111,596,597],{"class":124},"=>\n",[111,599,600,603,605,607,610,613,615,618,620,622,624,627],{"class":113,"line":204},[111,601,602],{"class":162},"  queryCollection",[111,604,424],{"class":128},[111,606,427],{"class":135},[111,608,609],{"class":128},").",[111,611,612],{"class":162},"path",[111,614,424],{"class":128},[111,616,617],{"class":135},"`\u002Fblog\u002F${",[111,619,588],{"class":128},[111,621,591],{"class":135},[111,623,609],{"class":128},[111,625,626],{"class":162},"first",[111,628,629],{"class":128},"()\n",[111,631,632],{"class":113,"line":240},[111,633,634],{"class":128},");\n",[10,636,638],{"id":637},"unocss-への移行","UnoCSS への移行",[14,640,641],{},"Astro では Tailwind CSS v4 を使っていましたが、Nuxt への移行にあわせて UnoCSS に変更しました。",[14,643,644,645,648],{},"UnoCSS はオンデマンドで CSS を生成するため、使用したユーティリティクラスだけがバンドルに含まれます。",[89,646,647],{},"presetWind4"," で Tailwind v4 互換のユーティリティが使えます。",[102,650,652],{"className":104,"code":651,"language":106,"meta":107,"style":107},"\u002F\u002F uno.config.ts\nimport { defineConfig, presetWind4 } from 'unocss';\n\nexport default defineConfig({\n  presets: [presetWind4()],\n});\n",[89,653,654,659,673,677,688,698],{"__ignoreMap":107},[111,655,656],{"class":113,"line":114},[111,657,658],{"class":117},"\u002F\u002F uno.config.ts\n",[111,660,661,663,666,668,671],{"class":113,"line":121},[111,662,125],{"class":124},[111,664,665],{"class":128}," { defineConfig, presetWind4 } ",[111,667,132],{"class":124},[111,669,670],{"class":135}," 'unocss'",[111,672,139],{"class":128},[111,674,675],{"class":113,"line":142},[111,676,146],{"emptyLinePlaceholder":145},[111,678,679,681,683,686],{"class":113,"line":149},[111,680,287],{"class":124},[111,682,290],{"class":124},[111,684,685],{"class":162}," defineConfig",[111,687,166],{"class":128},[111,689,690,693,695],{"class":113,"line":169},[111,691,692],{"class":128},"  presets: [",[111,694,647],{"class":162},[111,696,697],{"class":128},"()],\n",[111,699,700],{"class":113,"line":181},[111,701,249],{"class":128},[14,703,704,705,708],{},"Nuxt への組み込みは ",[89,706,707],{},"@unocss\u002Fnuxt"," モジュールで完結します。",[102,710,712],{"className":104,"code":711,"language":106,"meta":107,"style":107},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['@unocss\u002Fnuxt', '@nuxt\u002Fcontent'],\n});\n",[89,713,714,719,730,746],{"__ignoreMap":107},[111,715,716],{"class":113,"line":114},[111,717,718],{"class":117},"\u002F\u002F nuxt.config.ts\n",[111,720,721,723,725,728],{"class":113,"line":121},[111,722,287],{"class":124},[111,724,290],{"class":124},[111,726,727],{"class":162}," defineNuxtConfig",[111,729,166],{"class":128},[111,731,732,735,738,740,743],{"class":113,"line":142},[111,733,734],{"class":128},"  modules: [",[111,736,737],{"class":135},"'@unocss\u002Fnuxt'",[111,739,446],{"class":128},[111,741,742],{"class":135},"'@nuxt\u002Fcontent'",[111,744,745],{"class":128},"],\n",[111,747,748],{"class":113,"line":149},[111,749,249],{"class":128},[10,751,753],{"id":752},"vite-ツールチェーン","Vite+ ツールチェーン",[14,755,756,757,763,764,767],{},"このプロジェクトでは ",[758,759,49],"a",{"href":760,"rel":761},"https:\u002F\u002Fviteplus.dev",[762],"nofollow"," を使っています。Vite+は Vite \u002F Rolldown \u002F Vitest \u002F Oxlint \u002F Oxfmt を統合した ",[89,765,766],{},"vp"," コマンド 1 つで全てのツールを扱えるツールチェーンです。",[102,769,773],{"className":770,"code":771,"language":772,"meta":107,"style":107},"language-bash shiki shiki-themes github-light github-dark","# 開発サーバー起動\nvp dev\n\n# チェック（フォーマット・Lint・型チェック）\nvp check\n\n# テスト実行\nvp test\n","bash",[89,774,775,780,787,791,796,803,807,812],{"__ignoreMap":107},[111,776,777],{"class":113,"line":114},[111,778,779],{"class":117},"# 開発サーバー起動\n",[111,781,782,784],{"class":113,"line":121},[111,783,766],{"class":162},[111,785,786],{"class":135}," dev\n",[111,788,789],{"class":113,"line":142},[111,790,146],{"emptyLinePlaceholder":145},[111,792,793],{"class":113,"line":149},[111,794,795],{"class":117},"# チェック（フォーマット・Lint・型チェック）\n",[111,797,798,800],{"class":113,"line":169},[111,799,766],{"class":162},[111,801,802],{"class":135}," check\n",[111,804,805],{"class":113,"line":181},[111,806,146],{"emptyLinePlaceholder":145},[111,808,809],{"class":113,"line":192},[111,810,811],{"class":117},"# テスト実行\n",[111,813,814,816],{"class":113,"line":204},[111,815,766],{"class":162},[111,817,818],{"class":135}," test\n",[14,820,821],{},"個別にツールをインストール・設定する必要がなく、統一されたインターフェースでプロジェクトを管理できます。",[10,823,824],{"id":824},"ダークモード",[14,826,827],{},"システムの color-scheme に合わせた自動ダークモードを実装しています。",[14,829,830,831,834],{},"FOUC（コンテンツのちらつき）を防ぐため、インライン script を ",[89,832,833],{},"\u003Chead>"," 内で実行しています。",[102,836,838],{"className":104,"code":837,"language":106,"meta":107,"style":107},"\u002F\u002F nuxt.config.ts\napp: {\n  head: {\n    script: [{\n      innerHTML: `\n        const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n        document.documentElement.classList[isDark ? 'add' : 'remove']('dark');\n      `,\n      tagPosition: 'head',\n    }],\n  },\n},\n",[89,839,840,844,852,859,867,875,880,885,892,902,907,911],{"__ignoreMap":107},[111,841,842],{"class":113,"line":114},[111,843,718],{"class":117},[111,845,846,849],{"class":113,"line":121},[111,847,848],{"class":162},"app",[111,850,851],{"class":128},": {\n",[111,853,854,857],{"class":113,"line":142},[111,855,856],{"class":162},"  head",[111,858,851],{"class":128},[111,860,861,864],{"class":113,"line":149},[111,862,863],{"class":162},"    script",[111,865,866],{"class":128},": [{\n",[111,868,869,872],{"class":113,"line":169},[111,870,871],{"class":128},"      innerHTML: ",[111,873,874],{"class":135},"`\n",[111,876,877],{"class":113,"line":181},[111,878,879],{"class":135},"        const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n",[111,881,882],{"class":113,"line":192},[111,883,884],{"class":135},"        document.documentElement.classList[isDark ? 'add' : 'remove']('dark');\n",[111,886,887,890],{"class":113,"line":204},[111,888,889],{"class":135},"      `",[111,891,178],{"class":128},[111,893,894,897,900],{"class":113,"line":240},[111,895,896],{"class":128},"      tagPosition: ",[111,898,899],{"class":135},"'head'",[111,901,178],{"class":128},[111,903,904],{"class":113,"line":246},[111,905,906],{"class":128},"    }],\n",[111,908,909],{"class":113,"line":350},[111,910,375],{"class":128},[111,912,913],{"class":113,"line":360},[111,914,915],{"class":128},"},\n",[10,917,918],{"id":918},"まとめ",[14,920,921],{},"Astro から Nuxt Content v3 への移行で、Vue エコシステムに統一された環境を構築できました。",[30,923,924,929,934],{},[33,925,926,928],{},[26,927,43],{}," の SQLite ベースのクエリは高速で型安全",[33,930,931,933],{},[26,932,62],{}," のオンデマンド生成で CSS バンドルが最小化",[33,935,936,938],{},[26,937,49],{}," で複数ツールの設定を一元管理",[14,940,941],{},"Vue が好きな方には Nuxt Content の組み合わせを強くおすすめします。",[943,944,945],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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);}",{"title":107,"searchDepth":121,"depth":121,"links":947},[948,949,950,955,956,957,958],{"id":12,"depth":121,"text":12},{"id":22,"depth":121,"text":22},{"id":79,"depth":121,"text":80,"children":951},[952,953,954],{"id":84,"depth":142,"text":84},{"id":386,"depth":142,"text":386},{"id":467,"depth":142,"text":467},{"id":637,"depth":121,"text":638},{"id":752,"depth":121,"text":753},{"id":824,"depth":121,"text":824},{"id":918,"depth":121,"text":918},"2026-03-14","Astro 5 から Nuxt Content v3 + UnoCSS + Vite+ に移行した経緯と技術スタックの紹介です。","md",{},"\u002Fblog\u002Fnuxt-content-renewal",{"title":5,"description":960},"blog\u002Fnuxt-content-renewal","HMUgEHjcqGT6PHvlIGOALNvMYuxFxCANN5ZPFyd2LyQ",1773664053768]