[{"data":1,"prerenderedAt":1372},["ShallowReactive",2],{"blog-ja-2026-06-15-prerender-beasties":3},{"id":4,"title":5,"body":6,"category":1352,"date":1353,"description":1354,"draft":1355,"extension":1356,"lang":1357,"meta":1358,"navigation":359,"path":1365,"seo":1366,"sitemap":1367,"slug":1368,"stem":1369,"updated":1370,"__hash__":1371},"blog/blog/ja/2026-06-15-prerender-beasties.md","Nuxt 4：SSR + 全ページ prerender + Critical CSS inlining の落とし穴",{"type":7,"value":8,"toc":1333},"minimark",[9,14,23,37,40,68,75,79,86,91,131,137,144,147,154,161,217,224,228,238,241,245,257,447,451,459,501,509,515,518,969,973,984,1092,1099,1112,1122,1136,1144,1156,1166,1249,1253,1262,1267,1289,1300,1323,1329],[10,11,13],"h2",{"id":12},"背景","💡 背景",[15,16,17,18,22],"p",{},"本ブログは ",[19,20,21],"strong",{},"Nuxt 4 + Nuxt Content + Nuxt UI"," で構築しています。本番環境のデプロイモデルは次のとおりです。",[24,25,26,34],"ul",{},[27,28,29,33],"li",{},[30,31,32],"code",{},"ssr: true"," — SSR を維持し、開発体験と動的ルーティングに影響を与えない",[27,35,36],{},"公開コンテンツページはビルド時に全ページ prerender し、初回表示は静的 HTML と同等にする",[15,38,39],{},"この前提のもとでクリティカル CSS をインライン化する方法として、次の 3 つが妥当に見えました。",[41,42,43,52,60],"ol",{},[27,44,45,51],{},[19,46,47,50],{},[30,48,49],{},"render:response"," + Beasties"," — SSR レスポンス段階で HTML を書き換える",[27,53,54,59],{},[19,55,56],{},[30,57,58],{},"vite-plugin-beasties"," — Vite ビルド段階で処理する",[27,61,62],{},[19,63,64,67],{},[30,65,66],{},"nitro:build:public-assets"," で prerender 成果物に Beasties を実行する",[15,69,70,71,74],{},"本記事では、最初の 2 つがなぜうまくいかないのか、3 つ目がなぜ成立するのか、そして ",[30,72,73],{},"data-beasties-container"," による hydration の落とし穴を含む実装の詳細をまとめます。",[10,76,78],{"id":77},"最初の-2-つがうまくいかない理由","❌ 最初の 2 つがうまくいかない理由",[80,81,83,84,50],"h3",{"id":82},"_1-renderresponse-beasties","1. ",[30,85,49],{},[15,87,88,90],{},[30,89,49],{}," は Nitro の SSR レスポンスフックで、サーバーサイドレンダリング完了後・レスポンス送信前に実行されます。ビルド時の prerender 中にここへ Beasties を載せると、次の問題が起きます。",[41,92,93,118],{},[27,94,95,98,99,102,103,106,107,110,111,114,115,117],{},[19,96,97],{},"unhead リスク"," — Beasties は HTML 文字列を直接書き換えます（inline ",[30,100,101],{},"\u003Cstyle>"," の挿入、",[30,104,105],{},"\u003Clink>"," の変更）。これは ",[30,108,109],{},"unhead"," が ",[30,112,113],{},"\u003Chead>"," を管理するパイプラインの外で行われるため、出力順序が保証されず、クライアント hydration 時に ",[30,116,113],{}," 構造の乱れやスタイルの重複が起きやすくなります。",[27,119,120,123,124,127,128,130],{},[19,121,122],{},"Nuxt UI との競合"," — Nuxt UI は ",[30,125,126],{},"#nuxt-ui-colors"," などの inline style でテーマ色を書き込みます。Beasties による ",[30,129,113],{}," の書き換えがこれとぶつかり、hydration の不一致をさらに悪化させます。",[80,132,134,135],{"id":133},"_2-vite-plugin-beasties","2. ",[30,136,58],{},[15,138,139,140,143],{},"このプラグインは Vite ビルド段階で動作し、Vite が出力する HTML を処理します。Nuxt + Nitro のビルドチェーンでは、Vite がクライアントバンドルを先に完了し、その後 Nitro がルートごとに独立した ",[30,141,142],{},".html"," を prerender します。",[15,145,146],{},"プラグインが扱えるのは Vite のエントリ HTML だけで、Nitro prerender 後のマルチページ成果物ではありません。Vite が Nitro prerender より先に走るため、エントリ HTML を処理しても、最終的な各ページの prerender 成果物とは一致しません。",[10,148,150,151,153],{"id":149},"nitrobuildpublic-assets-beasties-が問題ない理由","✅ ",[30,152,66],{}," + Beasties が問題ない理由",[15,155,156,157,160],{},"本質的な違いは、",[19,158,159],{},"SSR レスポンスチェーン内で HTML を書き換えず、prerender がすべてディスクに書き終わったあと、静的成果物を後処理する","点にあります。",[41,162,163,179,192,198,211],{},[27,164,165,168,169,171,172,175,176,178],{},[19,166,167],{},"タイミングが合う"," — Nitro がルートごとに prerender し、各ページの ",[30,170,142],{}," を ",[30,173,174],{},"output.publicDir"," に書き込んだあと、",[30,177,66],{}," が実行されます。Beasties が読むのは CDN に載せる最終 HTML であり、Vite のエントリテンプレートでも prerender 前の中間成果物でもありません。",[27,180,181,184,185,188,189,191],{},[19,182,183],{},"カバレッジが完全"," — ",[30,186,187],{},"prerender:generate"," で成功した ",[30,190,142],{}," パスを記録し、ファイル単位で処理します。多言語コンテンツページ、一覧ページ、詳細ページそれぞれの prerender 成果物が対象になるため、「エントリ HTML だけ最適化された」問題を避けられます。",[27,193,194,197],{},[19,195,196],{},"SSR のホットパスを変えない"," — module は dev では完全にスキップします。本番でも prerender されていない動的ルートはこのフックに触れません。Beasties は登録済みの静的ページだけで動き、開発時とランタイム SSR の挙動はそのままです。",[27,199,200,203,204,210],{},[19,201,202],{},"unhead / Nuxt UI との競合を回避"," — 最初の 2 つの問題は、",[19,205,206,207,209],{},"head パイプラインが安定する前に ",[30,208,113],{}," を書き換える","ことが原因です。ビルド後処理では SSR が完了し、unhead と Nuxt UI の inline style がすでにファイルに書き込まれた静的 HTML を最適化します。レスポンス段階の head 管理と順序を奪い合いません。",[27,212,213,216],{},[19,214,215],{},"失敗を局所化できる"," — 1 ページの Beasties 処理が失敗しても warning のみで、ビルド全体は止まりません。外部 CSS は HTML に残るため、最低限フルスタイルのフォールバックがあります。",[15,218,219,220,223],{},"こうして責務が分離されます。",[19,221,222],{},"SSR / prerender が正しい HTML を生成し、Beasties はビルド末期に静的 HTML の初回表示を加速する"," — 「公開ページを全ページ prerender し、初回表示を静的 HTML 相当にする」というデプロイ目標と一致します。",[10,225,227],{"id":226},"️-実装","🛠️ 実装",[15,229,230,231,233,234,237],{},"前提：",[30,232,32],{}," と ",[30,235,236],{},"nitro.prerender"," の併用。\ndev は通常の SSR、本番の公開ページはビルド時に静的化します。",[15,239,240],{},"構成は 4 つ — Nuxt / Nitro 設定、Module 連携、Beasties 設定、hydration の整合です。",[80,242,244],{"id":243},"_1-nuxt-nitro-設定","1. Nuxt / Nitro 設定",[15,246,247,252,253,256],{},[19,248,249],{},[30,250,251],{},"nuxt.config.ts"," の prerender 関連設定（本番のみ有効。dev では ",[30,254,255],{},"routeRules"," を空にし、ローカルデバッグへの干渉を避ける）：",[258,259,264],"pre",{"className":260,"code":261,"language":262,"meta":263,"style":263},"language-ts shiki shiki-themes github-light github-light github-dark","ssr: true,\nrouteRules: isDev\n  ? {}\n  : {\n      \"/**\": { prerender: true }, // 本番環境で公開ページを全ページ prerender\n      \"/_vercel/image\": { prerender: false }, // Vercel 画像最適化ルート — 明示的に除外\n    },\n\nnitro: {\n  prerender: {\n    // prerender 済み HTML 内のサイト内 \u003Ca href> を走査し、リンクを自動追跡\n    crawlLinks: true,\n    // HTML 内に /_vercel/image?… が現れる場合（LCP preload など）— 誤 prerender を防ぐ\n    ignore: [/^\\/_vercel\\/image/],\n  },\n},\n","ts","",[30,265,266,286,294,304,313,332,348,354,361,370,378,384,396,402,435,441],{"__ignoreMap":263},[267,268,271,275,279,283],"span",{"class":269,"line":270},"line",1,[267,272,274],{"class":273},"snPdu","ssr",[267,276,278],{"class":277},"sxrX7",": ",[267,280,282],{"class":281},"sBjJW","true",[267,284,285],{"class":277},",\n",[267,287,289,291],{"class":269,"line":288},2,[267,290,255],{"class":273},[267,292,293],{"class":277},": isDev\n",[267,295,297,301],{"class":269,"line":296},3,[267,298,300],{"class":299},"s8jYJ","  ?",[267,302,303],{"class":277}," {}\n",[267,305,307,310],{"class":269,"line":306},4,[267,308,309],{"class":299},"  :",[267,311,312],{"class":277}," {\n",[267,314,316,320,323,325,328],{"class":269,"line":315},5,[267,317,319],{"class":318},"sIIMD","      \"/**\"",[267,321,322],{"class":277},": { prerender: ",[267,324,282],{"class":281},[267,326,327],{"class":277}," }, ",[267,329,331],{"class":330},"sCsY4","// 本番環境で公開ページを全ページ prerender\n",[267,333,335,338,340,343,345],{"class":269,"line":334},6,[267,336,337],{"class":318},"      \"/_vercel/image\"",[267,339,322],{"class":277},[267,341,342],{"class":281},"false",[267,344,327],{"class":277},[267,346,347],{"class":330},"// Vercel 画像最適化ルート — 明示的に除外\n",[267,349,351],{"class":269,"line":350},7,[267,352,353],{"class":277},"    },\n",[267,355,357],{"class":269,"line":356},8,[267,358,360],{"emptyLinePlaceholder":359},true,"\n",[267,362,364,367],{"class":269,"line":363},9,[267,365,366],{"class":273},"nitro",[267,368,369],{"class":277},": {\n",[267,371,373,376],{"class":269,"line":372},10,[267,374,375],{"class":273},"  prerender",[267,377,369],{"class":277},[267,379,381],{"class":269,"line":380},11,[267,382,383],{"class":330},"    // prerender 済み HTML 内のサイト内 \u003Ca href> を走査し、リンクを自動追跡\n",[267,385,387,390,392,394],{"class":269,"line":386},12,[267,388,389],{"class":273},"    crawlLinks",[267,391,278],{"class":277},[267,393,282],{"class":281},[267,395,285],{"class":277},[267,397,399],{"class":269,"line":398},13,[267,400,401],{"class":330},"    // HTML 内に /_vercel/image?… が現れる場合（LCP preload など）— 誤 prerender を防ぐ\n",[267,403,405,408,411,414,417,421,425,427,430,432],{"class":269,"line":404},14,[267,406,407],{"class":273},"    ignore",[267,409,410],{"class":277},": [",[267,412,413],{"class":318},"/",[267,415,416],{"class":299},"^",[267,418,420],{"class":419},"sujWL","\\/",[267,422,424],{"class":423},"s3JiQ","_vercel",[267,426,420],{"class":419},[267,428,429],{"class":423},"image",[267,431,413],{"class":318},[267,433,434],{"class":277},"],\n",[267,436,438],{"class":269,"line":437},15,[267,439,440],{"class":277},"  },\n",[267,442,444],{"class":269,"line":443},16,[267,445,446],{"class":277},"},\n",[80,448,450],{"id":449},"_2-module-設定","2. Module 設定",[15,452,453,458],{},[19,454,455],{},[30,456,457],{},"modules/build-prerender.ts"," は dev 以外で次の 3 つを行います。",[41,460,461,477,491],{},[27,462,463,468,469,472,473,476],{},[19,464,465],{},[30,466,467],{},"prerender:routes","\nコンテンツルートを prerender リストに追加するため、",[30,470,471],{},"collectPrerenderRoutes()"," を呼び出します。",[30,474,475],{},"crawlLinks: true"," の自動発見に加え、各 locale の静的ページと非 draft 記事を明示的に収集します。多言語サイトでは content ファイルパスと公開 URL が一致しないことが多く、crawler だけではページ漏れが起き得ます。",[27,478,479,483,484,486,487,490],{},[19,480,481],{},[30,482,187],{},"\n成功した ",[30,485,142],{}," パスを ",[30,488,489],{},"generatedFiles"," に記録します。",[27,492,493,497,498,500],{},[19,494,495],{},[30,496,66],{},"\n記録した ",[30,499,489],{}," を走査し、prerender 成功した HTML にだけ Beasties を実行します。",[15,502,503,504,508],{},"直感に反する前提が 1 つあります。公式ドキュメントでは「public assets のコピー後、Nitro server ビルド完了前」と説明されていますが、ここでの ",[505,506,507],"em",{},"server ビルド"," は本番 server を rollup でバンドルする工程を指し、Nitro ライフサイクル全体ではありません。実際の順序は prerender 完了 → HTML がディスクに落ちる → このフック、なので Beasties が読むのは最終 prerender 成果物です。",[15,510,511,512,514],{},"Beasties の入力はディスク上の最終 HTML です。",[30,513,489],{}," が空ならスキップし、ファイル単位の失敗は warning のみでビルドは継続します。",[15,516,517],{},"主要なコード構造：",[258,519,521],{"className":260,"code":520,"language":262,"meta":263,"style":263},"// prerender 成功でディスクに書き込まれた .html の相対パスを記録\nconst generatedFiles = new Set\u003Cstring>();\n\n// crawlLinks が漏らしうるコンテンツルートを補完（multi-locale、非 draft 記事など）\nnuxt.hooks.hook(\"prerender:routes\", async ({ routes }) => {\n  const discovered = await collectPrerenderRoutes(nuxt.options.rootDir);\n  for (const route of discovered) {\n    routes.add(route);\n  }\n});\n\n// Nitro インスタンス待ち — 初期化後に prerender:generate をマウント\nnuxt.hooks.hook(\"nitro:init\", (nitro) => {\n  // prerender 成功ごとに .html パスを登録\n  nitro.hooks.hook(\"prerender:generate\", (route) => {\n    if (!route.fileName?.endsWith(\".html\") || route.error) return;\n    generatedFiles.add(withoutLeadingSlash(route.fileName));\n  });\n});\n\n// prerender 完了・public assets コピー後、ファイル単位でクリティカル CSS をインライン化\nnuxt.hooks.hook(\"nitro:build:public-assets\", async (nitro) => {\n  const beasties = new Beasties({\n    path: nitro.options.output.publicDir,\n    publicPath: nitro.options.baseURL,\n    ...beastiesConfig,\n  });\n\n  for (const file of generatedFiles) {\n    const htmlPath = resolve(nitro.options.output.publicDir, file);\n    const contents = await readFile(htmlPath, \"utf-8\");\n    const processed = await beasties.process(contents);\n    await writeFile(htmlPath, processed); // ディスク上の prerender 成果物を上書き\n  }\n});\n",[30,522,523,528,554,558,563,598,617,636,647,652,657,661,666,689,694,717,752,768,774,779,784,790,816,834,840,846,855,860,865,882,899,923,944,959,964],{"__ignoreMap":263},[267,524,525],{"class":269,"line":270},[267,526,527],{"class":330},"// prerender 成功でディスクに書き込まれた .html の相対パスを記録\n",[267,529,530,533,536,539,542,545,548,551],{"class":269,"line":288},[267,531,532],{"class":299},"const",[267,534,535],{"class":281}," generatedFiles",[267,537,538],{"class":299}," =",[267,540,541],{"class":299}," new",[267,543,544],{"class":273}," Set",[267,546,547],{"class":277},"\u003C",[267,549,550],{"class":281},"string",[267,552,553],{"class":277},">();\n",[267,555,556],{"class":269,"line":296},[267,557,360],{"emptyLinePlaceholder":359},[267,559,560],{"class":269,"line":306},[267,561,562],{"class":330},"// crawlLinks が漏らしうるコンテンツルートを補完（multi-locale、非 draft 記事など）\n",[267,564,565,568,571,574,577,580,583,586,590,593,596],{"class":269,"line":315},[267,566,567],{"class":277},"nuxt.hooks.",[267,569,570],{"class":273},"hook",[267,572,573],{"class":277},"(",[267,575,576],{"class":318},"\"prerender:routes\"",[267,578,579],{"class":277},", ",[267,581,582],{"class":299},"async",[267,584,585],{"class":277}," ({ ",[267,587,589],{"class":588},"sP4rz","routes",[267,591,592],{"class":277}," }) ",[267,594,595],{"class":299},"=>",[267,597,312],{"class":277},[267,599,600,603,606,608,611,614],{"class":269,"line":334},[267,601,602],{"class":299},"  const",[267,604,605],{"class":281}," discovered",[267,607,538],{"class":299},[267,609,610],{"class":299}," await",[267,612,613],{"class":273}," collectPrerenderRoutes",[267,615,616],{"class":277},"(nuxt.options.rootDir);\n",[267,618,619,622,625,627,630,633],{"class":269,"line":350},[267,620,621],{"class":299},"  for",[267,623,624],{"class":277}," (",[267,626,532],{"class":299},[267,628,629],{"class":281}," route",[267,631,632],{"class":299}," of",[267,634,635],{"class":277}," discovered) {\n",[267,637,638,641,644],{"class":269,"line":356},[267,639,640],{"class":277},"    routes.",[267,642,643],{"class":273},"add",[267,645,646],{"class":277},"(route);\n",[267,648,649],{"class":269,"line":363},[267,650,651],{"class":277},"  }\n",[267,653,654],{"class":269,"line":372},[267,655,656],{"class":277},"});\n",[267,658,659],{"class":269,"line":380},[267,660,360],{"emptyLinePlaceholder":359},[267,662,663],{"class":269,"line":386},[267,664,665],{"class":330},"// Nitro インスタンス待ち — 初期化後に prerender:generate をマウント\n",[267,667,668,670,672,674,677,680,682,685,687],{"class":269,"line":398},[267,669,567],{"class":277},[267,671,570],{"class":273},[267,673,573],{"class":277},[267,675,676],{"class":318},"\"nitro:init\"",[267,678,679],{"class":277},", (",[267,681,366],{"class":588},[267,683,684],{"class":277},") ",[267,686,595],{"class":299},[267,688,312],{"class":277},[267,690,691],{"class":269,"line":404},[267,692,693],{"class":330},"  // prerender 成功ごとに .html パスを登録\n",[267,695,696,699,701,703,706,708,711,713,715],{"class":269,"line":437},[267,697,698],{"class":277},"  nitro.hooks.",[267,700,570],{"class":273},[267,702,573],{"class":277},[267,704,705],{"class":318},"\"prerender:generate\"",[267,707,679],{"class":277},[267,709,710],{"class":588},"route",[267,712,684],{"class":277},[267,714,595],{"class":299},[267,716,312],{"class":277},[267,718,719,722,724,727,730,733,735,738,740,743,746,749],{"class":269,"line":443},[267,720,721],{"class":299},"    if",[267,723,624],{"class":277},[267,725,726],{"class":299},"!",[267,728,729],{"class":277},"route.fileName?.",[267,731,732],{"class":273},"endsWith",[267,734,573],{"class":277},[267,736,737],{"class":318},"\".html\"",[267,739,684],{"class":277},[267,741,742],{"class":299},"||",[267,744,745],{"class":277}," route.error) ",[267,747,748],{"class":299},"return",[267,750,751],{"class":277},";\n",[267,753,755,758,760,762,765],{"class":269,"line":754},17,[267,756,757],{"class":277},"    generatedFiles.",[267,759,643],{"class":273},[267,761,573],{"class":277},[267,763,764],{"class":273},"withoutLeadingSlash",[267,766,767],{"class":277},"(route.fileName));\n",[267,769,771],{"class":269,"line":770},18,[267,772,773],{"class":277},"  });\n",[267,775,777],{"class":269,"line":776},19,[267,778,656],{"class":277},[267,780,782],{"class":269,"line":781},20,[267,783,360],{"emptyLinePlaceholder":359},[267,785,787],{"class":269,"line":786},21,[267,788,789],{"class":330},"// prerender 完了・public assets コピー後、ファイル単位でクリティカル CSS をインライン化\n",[267,791,793,795,797,799,802,804,806,808,810,812,814],{"class":269,"line":792},22,[267,794,567],{"class":277},[267,796,570],{"class":273},[267,798,573],{"class":277},[267,800,801],{"class":318},"\"nitro:build:public-assets\"",[267,803,579],{"class":277},[267,805,582],{"class":299},[267,807,624],{"class":277},[267,809,366],{"class":588},[267,811,684],{"class":277},[267,813,595],{"class":299},[267,815,312],{"class":277},[267,817,819,821,824,826,828,831],{"class":269,"line":818},23,[267,820,602],{"class":299},[267,822,823],{"class":281}," beasties",[267,825,538],{"class":299},[267,827,541],{"class":299},[267,829,830],{"class":273}," Beasties",[267,832,833],{"class":277},"({\n",[267,835,837],{"class":269,"line":836},24,[267,838,839],{"class":277},"    path: nitro.options.output.publicDir,\n",[267,841,843],{"class":269,"line":842},25,[267,844,845],{"class":277},"    publicPath: nitro.options.baseURL,\n",[267,847,849,852],{"class":269,"line":848},26,[267,850,851],{"class":299},"    ...",[267,853,854],{"class":277},"beastiesConfig,\n",[267,856,858],{"class":269,"line":857},27,[267,859,773],{"class":277},[267,861,863],{"class":269,"line":862},28,[267,864,360],{"emptyLinePlaceholder":359},[267,866,868,870,872,874,877,879],{"class":269,"line":867},29,[267,869,621],{"class":299},[267,871,624],{"class":277},[267,873,532],{"class":299},[267,875,876],{"class":281}," file",[267,878,632],{"class":299},[267,880,881],{"class":277}," generatedFiles) {\n",[267,883,885,888,891,893,896],{"class":269,"line":884},30,[267,886,887],{"class":299},"    const",[267,889,890],{"class":281}," htmlPath",[267,892,538],{"class":299},[267,894,895],{"class":273}," resolve",[267,897,898],{"class":277},"(nitro.options.output.publicDir, file);\n",[267,900,902,904,907,909,911,914,917,920],{"class":269,"line":901},31,[267,903,887],{"class":299},[267,905,906],{"class":281}," contents",[267,908,538],{"class":299},[267,910,610],{"class":299},[267,912,913],{"class":273}," readFile",[267,915,916],{"class":277},"(htmlPath, ",[267,918,919],{"class":318},"\"utf-8\"",[267,921,922],{"class":277},");\n",[267,924,926,928,931,933,935,938,941],{"class":269,"line":925},32,[267,927,887],{"class":299},[267,929,930],{"class":281}," processed",[267,932,538],{"class":299},[267,934,610],{"class":299},[267,936,937],{"class":277}," beasties.",[267,939,940],{"class":273},"process",[267,942,943],{"class":277},"(contents);\n",[267,945,947,950,953,956],{"class":269,"line":946},33,[267,948,949],{"class":299},"    await",[267,951,952],{"class":273}," writeFile",[267,954,955],{"class":277},"(htmlPath, processed); ",[267,957,958],{"class":330},"// ディスク上の prerender 成果物を上書き\n",[267,960,962],{"class":269,"line":961},34,[267,963,651],{"class":277},[267,965,967],{"class":269,"line":966},35,[267,968,656],{"class":277},[80,970,972],{"id":971},"_3-beasties-設定","3. Beasties 設定",[15,974,975,980,981,983],{},[19,976,977],{},[30,978,979],{},"config/beasties.ts"," の設定は保守的です。初回表示を加速しつつ、外部 CSS を唯一のソースにしません。各 ",[30,982,342],{}," の理由は行内コメントに記載しています。",[258,985,987],{"className":260,"code":986,"language":262,"meta":263,"style":263},"export const beastiesConfig = {\n  // クリティカル CSS のみインライン化し、外部 CSS の \u003Clink> タグは変更しない —\n  // Nuxt/unhead の head hydration との競合を避ける\n  preload: false,\n\n  // 外部 entry.css からインライン化済みのルールを削らない —\n  // クライアントナビゲーションとランタイムスタイルはフル CSS に依存\n  pruneSource: false,\n\n  // @font-face をインライン化せず、Beasties 経由でフォントを preload しない —\n  // フォントは既存の CSS / リソース戦略に任せる\n  fonts: false,\n\n  // 外部 \u003Clink> 由来の style のみ処理し、SSR 既存の inline style（Nuxt UI の #nuxt-ui-colors など）をスキップ\n  reduceInlineStyles: false,\n};\n",[30,988,989,1004,1009,1014,1023,1027,1032,1037,1046,1050,1055,1060,1069,1073,1078,1087],{"__ignoreMap":263},[267,990,991,994,997,1000,1002],{"class":269,"line":270},[267,992,993],{"class":299},"export",[267,995,996],{"class":299}," const",[267,998,999],{"class":281}," beastiesConfig",[267,1001,538],{"class":299},[267,1003,312],{"class":277},[267,1005,1006],{"class":269,"line":288},[267,1007,1008],{"class":330},"  // クリティカル CSS のみインライン化し、外部 CSS の \u003Clink> タグは変更しない —\n",[267,1010,1011],{"class":269,"line":296},[267,1012,1013],{"class":330},"  // Nuxt/unhead の head hydration との競合を避ける\n",[267,1015,1016,1019,1021],{"class":269,"line":306},[267,1017,1018],{"class":277},"  preload: ",[267,1020,342],{"class":281},[267,1022,285],{"class":277},[267,1024,1025],{"class":269,"line":315},[267,1026,360],{"emptyLinePlaceholder":359},[267,1028,1029],{"class":269,"line":334},[267,1030,1031],{"class":330},"  // 外部 entry.css からインライン化済みのルールを削らない —\n",[267,1033,1034],{"class":269,"line":350},[267,1035,1036],{"class":330},"  // クライアントナビゲーションとランタイムスタイルはフル CSS に依存\n",[267,1038,1039,1042,1044],{"class":269,"line":356},[267,1040,1041],{"class":277},"  pruneSource: ",[267,1043,342],{"class":281},[267,1045,285],{"class":277},[267,1047,1048],{"class":269,"line":363},[267,1049,360],{"emptyLinePlaceholder":359},[267,1051,1052],{"class":269,"line":372},[267,1053,1054],{"class":330},"  // @font-face をインライン化せず、Beasties 経由でフォントを preload しない —\n",[267,1056,1057],{"class":269,"line":380},[267,1058,1059],{"class":330},"  // フォントは既存の CSS / リソース戦略に任せる\n",[267,1061,1062,1065,1067],{"class":269,"line":386},[267,1063,1064],{"class":277},"  fonts: ",[267,1066,342],{"class":281},[267,1068,285],{"class":277},[267,1070,1071],{"class":269,"line":398},[267,1072,360],{"emptyLinePlaceholder":359},[267,1074,1075],{"class":269,"line":404},[267,1076,1077],{"class":330},"  // 外部 \u003Clink> 由来の style のみ処理し、SSR 既存の inline style（Nuxt UI の #nuxt-ui-colors など）をスキップ\n",[267,1079,1080,1083,1085],{"class":269,"line":437},[267,1081,1082],{"class":277},"  reduceInlineStyles: ",[267,1084,342],{"class":281},[267,1086,285],{"class":277},[267,1088,1089],{"class":269,"line":443},[267,1090,1091],{"class":277},"};\n",[80,1093,1095,1096,1098],{"id":1094},"_4-data-beasties-container-と-hydration","4. ",[30,1097,73],{}," と hydration",[15,1100,1101,1102,1104,1105,1111],{},"Beasties は ",[30,1103,73],{}," でクリティカル CSS の評価範囲を狭められます。公式では ",[19,1106,1107,1110],{},[30,1108,1109],{},"\u003Cbody>"," 内でファーストビューに見えるコンテンツを包む最外層コンテナ"," に付けることを推奨しており、そのサブツリーに対してだけ CSS セレクタをマッチさせ、大きなページや深くネストしたページのビルド時間短縮に使えます。",[15,1113,1114,1115,1118,1119,1121],{},"属性が見つからない場合、Beasties は ",[30,1116,1117],{},"\u003Chtml>"," にフォールバックし、",[30,1120,73],{}," を自動書き込みします — 実質的にドキュメント全体を評価するのと同じです。",[15,1123,1124,1125,1128,1129,1131,1132,1135],{},"本プロジェクトでは ",[30,1126,1127],{},"app/app.vue"," の ",[30,1130,1117],{}," に設定しています。主目的は範囲の限定ではなく ",[19,1133,1134],{},"hydration の整合"," です。",[24,1137,1138,1141],{},[27,1139,1140],{},"Beasties が prerender HTML を処理したあとも、出力に属性が残る",[27,1142,1143],{},"SSR テンプレート側に同じ属性がなければ、クライアント hydration で DOM 差分が起きうる",[15,1145,1146,1147,1149,1150,1152,1153,1155],{},"将来ページが長くなり、ビルド時 Beasties の処理時間が増えた場合は、",[30,1148,1117],{}," のままにせず、",[30,1151,1109],{}," 内のファーストビューコンテナ（hero + ナビを包む外側 div など）へ ",[30,1154,73],{}," を移す方が適切です。",[15,1157,1158,1128,1162,1165],{},[19,1159,1160],{},[30,1161,1127],{},[30,1163,1164],{},"useHead"," 関連部分：",[258,1167,1171],{"className":1168,"code":1169,"language":1170,"meta":263,"style":263},"language-vue shiki shiki-themes github-light github-light github-dark","\u003Cscript setup lang=\"ts\">\n  useHead(() => ({\n    htmlAttrs: {\n      // Beasties がビルド時 prerender で書き込む属性 — SSR 出力と一致させる\n      \"data-beasties-container\": \"\",\n    },\n  }));\n\u003C/script>\n","vue",[30,1172,1173,1196,1209,1214,1219,1231,1235,1240],{"__ignoreMap":263},[267,1174,1175,1177,1181,1184,1187,1190,1193],{"class":269,"line":270},[267,1176,547],{"class":277},[267,1178,1180],{"class":1179},"sovSZ","script",[267,1182,1183],{"class":273}," setup",[267,1185,1186],{"class":273}," lang",[267,1188,1189],{"class":277},"=",[267,1191,1192],{"class":318},"\"ts\"",[267,1194,1195],{"class":277},">\n",[267,1197,1198,1201,1204,1206],{"class":269,"line":288},[267,1199,1200],{"class":273},"  useHead",[267,1202,1203],{"class":277},"(() ",[267,1205,595],{"class":299},[267,1207,1208],{"class":277}," ({\n",[267,1210,1211],{"class":269,"line":296},[267,1212,1213],{"class":277},"    htmlAttrs: {\n",[267,1215,1216],{"class":269,"line":306},[267,1217,1218],{"class":330},"      // Beasties がビルド時 prerender で書き込む属性 — SSR 出力と一致させる\n",[267,1220,1221,1224,1226,1229],{"class":269,"line":315},[267,1222,1223],{"class":318},"      \"data-beasties-container\"",[267,1225,278],{"class":277},[267,1227,1228],{"class":318},"\"\"",[267,1230,285],{"class":277},[267,1232,1233],{"class":269,"line":334},[267,1234,353],{"class":277},[267,1236,1237],{"class":269,"line":350},[267,1238,1239],{"class":277},"  }));\n",[267,1241,1242,1245,1247],{"class":269,"line":356},[267,1243,1244],{"class":277},"\u003C/",[267,1246,1180],{"class":1179},[267,1248,1195],{"class":277},[10,1250,1252],{"id":1251},"まとめ","📋 まとめ",[15,1254,1255,1258,1259,1135],{},[19,1256,1257],{},"SSR 有効 + 公開ページ全ページ prerender"," の前提でクリティカル CSS をインライン化する際、要点は「Beasties が使えるか」ではなく、",[19,1260,1261],{},"いつ、どの HTML に対して実行するか",[15,1263,1264],{},[19,1265,1266],{},"うまくいかない 2 つの方法：",[24,1268,1269,1282],{},[27,1270,1271,1275,1276,1278,1279,1281],{},[19,1272,1273,50],{},[30,1274,49],{}," — SSR レスポンスチェーンで ",[30,1277,113],{}," を直接書き換え、unhead パイプラインの外で動作。Nuxt UI の inline style（",[30,1280,126],{}," など）と競合し、hydration リスクが高い。",[27,1283,1284,1288],{},[19,1285,1286],{},[30,1287,58],{}," — Vite ビルドが Nitro prerender より先。プラグインは Vite エントリ HTML のみ処理し、ルートごとの prerender 成果物をカバーできない。",[15,1290,1291,1294,1295,1299],{},[19,1292,1293],{},"成立する方法："," ",[19,1296,1297],{},[30,1298,66],{}," で prerender 済み HTML に Beasties を実行 — dev では module をスキップ、本番でも SSR ホットパスには触れない。SSR / prerender が正しいページを生成し、Beasties はデプロイ成果物の初回表示だけを加速する。",[15,1301,1302,1294,1305,1308,1309,1312,1313,1315,1316,1318,1319,1322],{},[19,1303,1304],{},"検証：",[30,1306,1307],{},"npm run build"," 後、",[30,1310,1311],{},".output/public/index.html"," に inline ",[30,1314,101],{}," と残存する外部 stylesheet の ",[30,1317,105],{}," があることを確認。ビルドログに ",[30,1320,1321],{},"Beasties processed N prerendered HTML file(s)"," が出力されること。",[15,1324,1325,1328],{},[19,1326,1327],{},"スコープ："," Beasties が改善するのは CSS による初回描画のブロックのみです。画像 LCP、JS hydration の遅延、API キャッシュなどは別レイヤーでの最適化が必要です。",[1330,1331,1332],"style",{},"html pre.shiki code .snPdu, html code.shiki .snPdu{--shiki-light:#6F42C1;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sxrX7, html code.shiki .sxrX7{--shiki-light:#24292E;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sBjJW, html code.shiki .sBjJW{--shiki-light:#005CC5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s8jYJ, html code.shiki .s8jYJ{--shiki-light:#D73A49;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIIMD, html code.shiki .sIIMD{--shiki-light:#032F62;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sCsY4, html code.shiki .sCsY4{--shiki-light:#6A737D;--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sujWL, html code.shiki .sujWL{--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .s3JiQ, html code.shiki .s3JiQ{--shiki-light:#032F62;--shiki-default:#032F62;--shiki-dark:#DBEDFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sP4rz, html code.shiki .sP4rz{--shiki-light:#E36209;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sovSZ, html code.shiki .sovSZ{--shiki-light:#22863A;--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":263,"searchDepth":288,"depth":288,"links":1334},[1335,1336,1342,1344,1351],{"id":12,"depth":288,"text":13},{"id":77,"depth":288,"text":78,"children":1337},[1338,1340],{"id":82,"depth":296,"text":1339},"1. render:response + Beasties",{"id":133,"depth":296,"text":1341},"2. vite-plugin-beasties",{"id":149,"depth":288,"text":1343},"✅ nitro:build:public-assets + Beasties が問題ない理由",{"id":226,"depth":288,"text":227,"children":1345},[1346,1347,1348,1349],{"id":243,"depth":296,"text":244},{"id":449,"depth":296,"text":450},{"id":971,"depth":296,"text":972},{"id":1094,"depth":296,"text":1350},"4. data-beasties-container と hydration",{"id":1251,"depth":288,"text":1252},"architecture","2026-06-15T00:00:00.000Z","Nuxt 4 で SSR を有効にし、公開ページを全ページ prerender したうえでクリティカル CSS をインライン化する：\nrender:response + Beasties → unhead / Nuxt UI の inline style と競合 ❌\nvite-plugin-beasties → Vite のエントリ HTML のみ処理し、ルートごとの prerender 成果物は対象外 ❌\nnitro:build:public-assets + Beasties ✅",false,"md","ja",{"tags":1359},[1360,1361,1362,1363,1364],"nuxt","prerender","beasties","critical-css","frontend-architecture","/blog/ja/2026-06-15-prerender-beasties",{"title":5,"description":1354},{"loc":1365},null,"blog/ja/2026-06-15-prerender-beasties","2026-06-21T00:00:00.000Z","HQXSsMea9LqgyTtt75phKWBqIImckZPpzPhtlsVPTEM",1784966029638]