[{"data":1,"prerenderedAt":1369},["ShallowReactive",2],{"blog-en-2026-06-15-prerender-beasties":3},{"id":4,"title":5,"body":6,"category":1349,"date":1350,"description":1351,"draft":1352,"extension":1353,"lang":1354,"meta":1355,"navigation":352,"path":1362,"seo":1363,"sitemap":1364,"slug":1365,"stem":1366,"updated":1367,"__hash__":1368},"blog/blog/en/2026-06-15-prerender-beasties.md","Nuxt 4: SSR, Full Prerender, and Inline Critical CSS — What Worked and What Didn't",{"type":7,"value":8,"toc":1330},"minimark",[9,14,23,37,40,69,76,80,87,92,128,134,141,144,151,157,209,216,220,230,233,237,250,440,444,452,494,502,508,511,962,966,977,1090,1096,1109,1119,1132,1140,1151,1162,1245,1249,1259,1264,1286,1297,1320,1326],[10,11,13],"h2",{"id":12},"background","💡 Background",[15,16,17,18,22],"p",{},"This blog runs on ",[19,20,21],"strong",{},"Nuxt 4 + Nuxt Content + Nuxt UI",". In production, the deployment model is:",[24,25,26,34],"ul",{},[27,28,29,33],"li",{},[30,31,32],"code",{},"ssr: true"," — SSR stays on so dev experience and dynamic routing are unaffected",[27,35,36],{},"Public content pages are fully prerendered at build time, so first paint behaves like static HTML",[15,38,39],{},"On top of that, we wanted to inline critical CSS. Three approaches looked reasonable:",[41,42,43,52,60],"ol",{},[27,44,45,51],{},[19,46,47,50],{},[30,48,49],{},"render:response"," + Beasties"," — mutate HTML during the SSR response phase",[27,53,54,59],{},[19,55,56],{},[30,57,58],{},"vite-plugin-beasties"," — run during the Vite build",[27,61,62,68],{},[19,63,64,65],{},"Run Beasties in ",[30,66,67],{},"nitro:build:public-assets"," against prerender output",[15,70,71,72,75],{},"This post explains why the first two fail, why the third works, and the implementation details — including the ",[30,73,74],{},"data-beasties-container"," hydration pitfall.",[10,77,79],{"id":78},"why-the-first-two-approaches-fail","❌ Why the First Two Approaches Fail",[81,82,84,85,50],"h3",{"id":83},"_1-renderresponse-beasties","1. ",[30,86,49],{},[15,88,89,91],{},[30,90,49],{}," is a Nitro SSR hook that runs after server rendering completes and before the response is sent. Hooking Beasties here during build-time prerender causes:",[41,93,94,115],{},[27,95,96,99,100,103,104,107,108,111,112,114],{},[19,97,98],{},"unhead risk"," — Beasties mutates the HTML string directly (inserting inline ",[30,101,102],{},"\u003Cstyle>",", rewriting ",[30,105,106],{},"\u003Clink>"," tags) outside unhead's ",[30,109,110],{},"\u003Chead>"," pipeline. Output order is no longer guaranteed, and client hydration can end up with a broken ",[30,113,110],{}," structure or duplicated styles.",[27,116,117,120,121,124,125,127],{},[19,118,119],{},"Nuxt UI conflicts"," — Nuxt UI writes theme colors via inline styles such as ",[30,122,123],{},"#nuxt-ui-colors",". Beasties' ",[30,126,110],{}," rewrites can clash with those, making hydration mismatches worse.",[81,129,131,132],{"id":130},"_2-vite-plugin-beasties","2. ",[30,133,58],{},[15,135,136,137,140],{},"This plugin runs during the Vite build and processes HTML files Vite emits. In a Nuxt + Nitro pipeline, Vite finishes the client bundle first; Nitro then prerenders each route into separate ",[30,138,139],{},".html"," files.",[15,142,143],{},"The plugin only sees Vite's entry HTML — not Nitro's per-route prerender output. Vite runs before Nitro prerender, so even a processed entry HTML is not the same as the final per-page prerender artifacts.",[10,145,147,148,150],{"id":146},"why-nitrobuildpublic-assets-beasties-works","✅ Why ",[30,149,67],{}," + Beasties Works",[15,152,153,154],{},"The key difference: ",[19,155,156],{},"don't mutate HTML in the SSR response chain — post-process static files after prerender has finished writing them to disk.",[41,158,159,175,188,194,203],{},[27,160,161,164,165,167,168,171,172,174],{},[19,162,163],{},"Correct timing"," — Nitro prerenders each route and writes ",[30,166,139],{}," files to ",[30,169,170],{},"output.publicDir",", then ",[30,173,67],{}," runs. Beasties reads the HTML that will actually ship to the CDN — not the Vite entry template, and not a pre-prerender intermediate.",[27,176,177,180,181,184,185,187],{},[19,178,179],{},"Full coverage"," — ",[30,182,183],{},"prerender:generate"," records every successfully generated ",[30,186,139],{}," path for per-file processing. Multilingual content pages, list pages, and detail pages each get their own prerender output, so you avoid the \"only the entry HTML was optimized\" problem.",[27,189,190,193],{},[19,191,192],{},"SSR hot path untouched"," — the module skips entirely in dev. In production, dynamic routes that were not prerendered never hit this hook. Beasties only runs on registered static pages; dev and runtime SSR behavior stay the same.",[27,195,196,199,200,202],{},[19,197,198],{},"Avoids unhead / Nuxt UI conflicts"," — the first two approaches fail because they rewrite ",[30,201,110],{}," before the head pipeline has settled. Post-build processing works on static HTML where SSR, unhead, and Nuxt UI inline styles are already written to disk. You optimize deployment artifacts, not compete with response-time head management.",[27,204,205,208],{},[19,206,207],{},"Failures are isolated"," — if Beasties fails on one page, it logs a warning and does not block the build. External CSS remains in the HTML, so the page still has full styles as a fallback.",[15,210,211,212,215],{},"That keeps responsibilities clear: ",[19,213,214],{},"SSR / prerender produces correct HTML; Beasties speeds up first paint on static HTML at the end of the build"," — aligned with \"full prerender of public pages, first paint like static HTML.\"",[10,217,219],{"id":218},"️-implementation","🛠️ Implementation",[15,221,222,223,225,226,229],{},"Prerequisite: ",[30,224,32],{}," alongside ",[30,227,228],{},"nitro.prerender",".\nDev is plain SSR; production public pages are static at build time.",[15,231,232],{},"Four pieces: Nuxt / Nitro config, module wiring, Beasties config, and hydration alignment.",[81,234,236],{"id":235},"_1-nuxt-nitro-config","1. Nuxt / Nitro Config",[15,238,239,240,245,246,249],{},"Prerender-related settings in ",[19,241,242],{},[30,243,244],{},"nuxt.config.ts"," (production only; dev ",[30,247,248],{},"routeRules"," stay empty so local debugging is unaffected):",[251,252,257],"pre",{"className":253,"code":254,"language":255,"meta":256,"style":256},"language-ts shiki shiki-themes github-light github-light github-dark","ssr: true,\nrouteRules: isDev\n  ? {}\n  : {\n      \"/**\": { prerender: true }, // full prerender of public pages in production\n      \"/_vercel/image\": { prerender: false }, // Vercel image optimization route — explicitly excluded\n    },\n\nnitro: {\n  prerender: {\n    // Scan prerendered HTML for in-site \u003Ca href> links and follow them automatically\n    crawlLinks: true,\n    // HTML may reference /_vercel/image?… (e.g. LCP preload); exclude to avoid accidental prerender\n    ignore: [/^\\/_vercel\\/image/],\n  },\n},\n","ts","",[30,258,259,279,287,297,306,325,341,347,354,363,371,377,389,395,428,434],{"__ignoreMap":256},[260,261,264,268,272,276],"span",{"class":262,"line":263},"line",1,[260,265,267],{"class":266},"snPdu","ssr",[260,269,271],{"class":270},"sxrX7",": ",[260,273,275],{"class":274},"sBjJW","true",[260,277,278],{"class":270},",\n",[260,280,282,284],{"class":262,"line":281},2,[260,283,248],{"class":266},[260,285,286],{"class":270},": isDev\n",[260,288,290,294],{"class":262,"line":289},3,[260,291,293],{"class":292},"s8jYJ","  ?",[260,295,296],{"class":270}," {}\n",[260,298,300,303],{"class":262,"line":299},4,[260,301,302],{"class":292},"  :",[260,304,305],{"class":270}," {\n",[260,307,309,313,316,318,321],{"class":262,"line":308},5,[260,310,312],{"class":311},"sIIMD","      \"/**\"",[260,314,315],{"class":270},": { prerender: ",[260,317,275],{"class":274},[260,319,320],{"class":270}," }, ",[260,322,324],{"class":323},"sCsY4","// full prerender of public pages in production\n",[260,326,328,331,333,336,338],{"class":262,"line":327},6,[260,329,330],{"class":311},"      \"/_vercel/image\"",[260,332,315],{"class":270},[260,334,335],{"class":274},"false",[260,337,320],{"class":270},[260,339,340],{"class":323},"// Vercel image optimization route — explicitly excluded\n",[260,342,344],{"class":262,"line":343},7,[260,345,346],{"class":270},"    },\n",[260,348,350],{"class":262,"line":349},8,[260,351,353],{"emptyLinePlaceholder":352},true,"\n",[260,355,357,360],{"class":262,"line":356},9,[260,358,359],{"class":266},"nitro",[260,361,362],{"class":270},": {\n",[260,364,366,369],{"class":262,"line":365},10,[260,367,368],{"class":266},"  prerender",[260,370,362],{"class":270},[260,372,374],{"class":262,"line":373},11,[260,375,376],{"class":323},"    // Scan prerendered HTML for in-site \u003Ca href> links and follow them automatically\n",[260,378,380,383,385,387],{"class":262,"line":379},12,[260,381,382],{"class":266},"    crawlLinks",[260,384,271],{"class":270},[260,386,275],{"class":274},[260,388,278],{"class":270},[260,390,392],{"class":262,"line":391},13,[260,393,394],{"class":323},"    // HTML may reference /_vercel/image?… (e.g. LCP preload); exclude to avoid accidental prerender\n",[260,396,398,401,404,407,410,414,418,420,423,425],{"class":262,"line":397},14,[260,399,400],{"class":266},"    ignore",[260,402,403],{"class":270},": [",[260,405,406],{"class":311},"/",[260,408,409],{"class":292},"^",[260,411,413],{"class":412},"sujWL","\\/",[260,415,417],{"class":416},"s3JiQ","_vercel",[260,419,413],{"class":412},[260,421,422],{"class":416},"image",[260,424,406],{"class":311},[260,426,427],{"class":270},"],\n",[260,429,431],{"class":262,"line":430},15,[260,432,433],{"class":270},"  },\n",[260,435,437],{"class":262,"line":436},16,[260,438,439],{"class":270},"},\n",[81,441,443],{"id":442},"_2-module-setup","2. Module Setup",[15,445,446,451],{},[19,447,448],{},[30,449,450],{},"modules/build-prerender.ts"," does three things outside dev:",[41,453,454,470,484],{},[27,455,456,461,462,465,466,469],{},[19,457,458],{},[30,459,460],{},"prerender:routes","\nCalls ",[30,463,464],{},"collectPrerenderRoutes()"," to add content routes to the prerender list. Besides ",[30,467,468],{},"crawlLinks: true",", it explicitly collects static pages and non-draft posts per locale. On a multilingual site, content file paths often do not match public URLs one-to-one; relying on the crawler alone can miss pages.",[27,471,472,476,477,479,480,483],{},[19,473,474],{},[30,475,183],{},"\nRecords successfully generated ",[30,478,139],{}," paths in ",[30,481,482],{},"generatedFiles",".",[27,485,486,490,491,493],{},[19,487,488],{},[30,489,67],{},"\nIterates ",[30,492,482],{}," and runs Beasties only on prerendered HTML that succeeded.",[15,495,496,497,501],{},"One non-obvious detail: official docs describe this hook as running \"after public assets are copied, before the Nitro server build completes.\" Here, ",[498,499,500],"em",{},"server build"," means rollup bundling the production server — not the entire Nitro lifecycle. In practice, prerender finishes and HTML is on disk before this hook runs, so Beasties reads final prerender output.",[15,503,504,505,507],{},"Beasties input is final HTML from disk. If ",[30,506,482],{}," is empty, processing is skipped. Per-file failures log a warning without failing the build.",[15,509,510],{},"Core structure:",[251,512,514],{"className":253,"code":513,"language":255,"meta":256,"style":256},"// Record relative paths of successfully prerendered .html files\nconst generatedFiles = new Set\u003Cstring>();\n\n// Supplement routes crawlLinks may miss (multi-locale, non-draft posts, etc.)\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// Wait for Nitro init, then attach prerender:generate\nnuxt.hooks.hook(\"nitro:init\", (nitro) => {\n  // Register each successfully prerendered page's .html path\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// After prerender completes and public assets are copied, inline critical CSS per file\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); // overwrite prerender output on disk\n  }\n});\n",[30,515,516,521,547,551,556,591,610,629,640,645,650,654,659,682,687,710,745,761,767,772,777,783,809,827,833,839,848,853,858,875,892,916,937,952,957],{"__ignoreMap":256},[260,517,518],{"class":262,"line":263},[260,519,520],{"class":323},"// Record relative paths of successfully prerendered .html files\n",[260,522,523,526,529,532,535,538,541,544],{"class":262,"line":281},[260,524,525],{"class":292},"const",[260,527,528],{"class":274}," generatedFiles",[260,530,531],{"class":292}," =",[260,533,534],{"class":292}," new",[260,536,537],{"class":266}," Set",[260,539,540],{"class":270},"\u003C",[260,542,543],{"class":274},"string",[260,545,546],{"class":270},">();\n",[260,548,549],{"class":262,"line":289},[260,550,353],{"emptyLinePlaceholder":352},[260,552,553],{"class":262,"line":299},[260,554,555],{"class":323},"// Supplement routes crawlLinks may miss (multi-locale, non-draft posts, etc.)\n",[260,557,558,561,564,567,570,573,576,579,583,586,589],{"class":262,"line":308},[260,559,560],{"class":270},"nuxt.hooks.",[260,562,563],{"class":266},"hook",[260,565,566],{"class":270},"(",[260,568,569],{"class":311},"\"prerender:routes\"",[260,571,572],{"class":270},", ",[260,574,575],{"class":292},"async",[260,577,578],{"class":270}," ({ ",[260,580,582],{"class":581},"sP4rz","routes",[260,584,585],{"class":270}," }) ",[260,587,588],{"class":292},"=>",[260,590,305],{"class":270},[260,592,593,596,599,601,604,607],{"class":262,"line":327},[260,594,595],{"class":292},"  const",[260,597,598],{"class":274}," discovered",[260,600,531],{"class":292},[260,602,603],{"class":292}," await",[260,605,606],{"class":266}," collectPrerenderRoutes",[260,608,609],{"class":270},"(nuxt.options.rootDir);\n",[260,611,612,615,618,620,623,626],{"class":262,"line":343},[260,613,614],{"class":292},"  for",[260,616,617],{"class":270}," (",[260,619,525],{"class":292},[260,621,622],{"class":274}," route",[260,624,625],{"class":292}," of",[260,627,628],{"class":270}," discovered) {\n",[260,630,631,634,637],{"class":262,"line":349},[260,632,633],{"class":270},"    routes.",[260,635,636],{"class":266},"add",[260,638,639],{"class":270},"(route);\n",[260,641,642],{"class":262,"line":356},[260,643,644],{"class":270},"  }\n",[260,646,647],{"class":262,"line":365},[260,648,649],{"class":270},"});\n",[260,651,652],{"class":262,"line":373},[260,653,353],{"emptyLinePlaceholder":352},[260,655,656],{"class":262,"line":379},[260,657,658],{"class":323},"// Wait for Nitro init, then attach prerender:generate\n",[260,660,661,663,665,667,670,673,675,678,680],{"class":262,"line":391},[260,662,560],{"class":270},[260,664,563],{"class":266},[260,666,566],{"class":270},[260,668,669],{"class":311},"\"nitro:init\"",[260,671,672],{"class":270},", (",[260,674,359],{"class":581},[260,676,677],{"class":270},") ",[260,679,588],{"class":292},[260,681,305],{"class":270},[260,683,684],{"class":262,"line":397},[260,685,686],{"class":323},"  // Register each successfully prerendered page's .html path\n",[260,688,689,692,694,696,699,701,704,706,708],{"class":262,"line":430},[260,690,691],{"class":270},"  nitro.hooks.",[260,693,563],{"class":266},[260,695,566],{"class":270},[260,697,698],{"class":311},"\"prerender:generate\"",[260,700,672],{"class":270},[260,702,703],{"class":581},"route",[260,705,677],{"class":270},[260,707,588],{"class":292},[260,709,305],{"class":270},[260,711,712,715,717,720,723,726,728,731,733,736,739,742],{"class":262,"line":436},[260,713,714],{"class":292},"    if",[260,716,617],{"class":270},[260,718,719],{"class":292},"!",[260,721,722],{"class":270},"route.fileName?.",[260,724,725],{"class":266},"endsWith",[260,727,566],{"class":270},[260,729,730],{"class":311},"\".html\"",[260,732,677],{"class":270},[260,734,735],{"class":292},"||",[260,737,738],{"class":270}," route.error) ",[260,740,741],{"class":292},"return",[260,743,744],{"class":270},";\n",[260,746,748,751,753,755,758],{"class":262,"line":747},17,[260,749,750],{"class":270},"    generatedFiles.",[260,752,636],{"class":266},[260,754,566],{"class":270},[260,756,757],{"class":266},"withoutLeadingSlash",[260,759,760],{"class":270},"(route.fileName));\n",[260,762,764],{"class":262,"line":763},18,[260,765,766],{"class":270},"  });\n",[260,768,770],{"class":262,"line":769},19,[260,771,649],{"class":270},[260,773,775],{"class":262,"line":774},20,[260,776,353],{"emptyLinePlaceholder":352},[260,778,780],{"class":262,"line":779},21,[260,781,782],{"class":323},"// After prerender completes and public assets are copied, inline critical CSS per file\n",[260,784,786,788,790,792,795,797,799,801,803,805,807],{"class":262,"line":785},22,[260,787,560],{"class":270},[260,789,563],{"class":266},[260,791,566],{"class":270},[260,793,794],{"class":311},"\"nitro:build:public-assets\"",[260,796,572],{"class":270},[260,798,575],{"class":292},[260,800,617],{"class":270},[260,802,359],{"class":581},[260,804,677],{"class":270},[260,806,588],{"class":292},[260,808,305],{"class":270},[260,810,812,814,817,819,821,824],{"class":262,"line":811},23,[260,813,595],{"class":292},[260,815,816],{"class":274}," beasties",[260,818,531],{"class":292},[260,820,534],{"class":292},[260,822,823],{"class":266}," Beasties",[260,825,826],{"class":270},"({\n",[260,828,830],{"class":262,"line":829},24,[260,831,832],{"class":270},"    path: nitro.options.output.publicDir,\n",[260,834,836],{"class":262,"line":835},25,[260,837,838],{"class":270},"    publicPath: nitro.options.baseURL,\n",[260,840,842,845],{"class":262,"line":841},26,[260,843,844],{"class":292},"    ...",[260,846,847],{"class":270},"beastiesConfig,\n",[260,849,851],{"class":262,"line":850},27,[260,852,766],{"class":270},[260,854,856],{"class":262,"line":855},28,[260,857,353],{"emptyLinePlaceholder":352},[260,859,861,863,865,867,870,872],{"class":262,"line":860},29,[260,862,614],{"class":292},[260,864,617],{"class":270},[260,866,525],{"class":292},[260,868,869],{"class":274}," file",[260,871,625],{"class":292},[260,873,874],{"class":270}," generatedFiles) {\n",[260,876,878,881,884,886,889],{"class":262,"line":877},30,[260,879,880],{"class":292},"    const",[260,882,883],{"class":274}," htmlPath",[260,885,531],{"class":292},[260,887,888],{"class":266}," resolve",[260,890,891],{"class":270},"(nitro.options.output.publicDir, file);\n",[260,893,895,897,900,902,904,907,910,913],{"class":262,"line":894},31,[260,896,880],{"class":292},[260,898,899],{"class":274}," contents",[260,901,531],{"class":292},[260,903,603],{"class":292},[260,905,906],{"class":266}," readFile",[260,908,909],{"class":270},"(htmlPath, ",[260,911,912],{"class":311},"\"utf-8\"",[260,914,915],{"class":270},");\n",[260,917,919,921,924,926,928,931,934],{"class":262,"line":918},32,[260,920,880],{"class":292},[260,922,923],{"class":274}," processed",[260,925,531],{"class":292},[260,927,603],{"class":292},[260,929,930],{"class":270}," beasties.",[260,932,933],{"class":266},"process",[260,935,936],{"class":270},"(contents);\n",[260,938,940,943,946,949],{"class":262,"line":939},33,[260,941,942],{"class":292},"    await",[260,944,945],{"class":266}," writeFile",[260,947,948],{"class":270},"(htmlPath, processed); ",[260,950,951],{"class":323},"// overwrite prerender output on disk\n",[260,953,955],{"class":262,"line":954},34,[260,956,644],{"class":270},[260,958,960],{"class":262,"line":959},35,[260,961,649],{"class":270},[81,963,965],{"id":964},"_3-beasties-config","3. Beasties Config",[15,967,968,973,974,976],{},[19,969,970],{},[30,971,972],{},"config/beasties.ts"," uses conservative settings: speed up first paint without treating external CSS as the sole source. Inline comments explain each ",[30,975,335],{},":",[251,978,980],{"className":253,"code":979,"language":255,"meta":256,"style":256},"export const beastiesConfig = {\n  // Inline critical CSS only; do not change external \u003Clink> tags —\n  // avoids fighting Nuxt/unhead head hydration\n  preload: false,\n\n  // Do not prune rules already inlined from external entry.css;\n  // client navigation and runtime styles still rely on the full CSS fallback\n  pruneSource: false,\n\n  // Do not inline @font-face or preload fonts via Beasties;\n  // fonts follow existing CSS / asset strategy\n  fonts: false,\n\n  // Process only styles from external \u003Clink> tags;\n  // skip SSR inline styles (e.g. Nuxt UI #nuxt-ui-colors)\n  reduceInlineStyles: false,\n};\n",[30,981,982,997,1002,1007,1016,1020,1025,1030,1039,1043,1048,1053,1062,1066,1071,1076,1085],{"__ignoreMap":256},[260,983,984,987,990,993,995],{"class":262,"line":263},[260,985,986],{"class":292},"export",[260,988,989],{"class":292}," const",[260,991,992],{"class":274}," beastiesConfig",[260,994,531],{"class":292},[260,996,305],{"class":270},[260,998,999],{"class":262,"line":281},[260,1000,1001],{"class":323},"  // Inline critical CSS only; do not change external \u003Clink> tags —\n",[260,1003,1004],{"class":262,"line":289},[260,1005,1006],{"class":323},"  // avoids fighting Nuxt/unhead head hydration\n",[260,1008,1009,1012,1014],{"class":262,"line":299},[260,1010,1011],{"class":270},"  preload: ",[260,1013,335],{"class":274},[260,1015,278],{"class":270},[260,1017,1018],{"class":262,"line":308},[260,1019,353],{"emptyLinePlaceholder":352},[260,1021,1022],{"class":262,"line":327},[260,1023,1024],{"class":323},"  // Do not prune rules already inlined from external entry.css;\n",[260,1026,1027],{"class":262,"line":343},[260,1028,1029],{"class":323},"  // client navigation and runtime styles still rely on the full CSS fallback\n",[260,1031,1032,1035,1037],{"class":262,"line":349},[260,1033,1034],{"class":270},"  pruneSource: ",[260,1036,335],{"class":274},[260,1038,278],{"class":270},[260,1040,1041],{"class":262,"line":356},[260,1042,353],{"emptyLinePlaceholder":352},[260,1044,1045],{"class":262,"line":365},[260,1046,1047],{"class":323},"  // Do not inline @font-face or preload fonts via Beasties;\n",[260,1049,1050],{"class":262,"line":373},[260,1051,1052],{"class":323},"  // fonts follow existing CSS / asset strategy\n",[260,1054,1055,1058,1060],{"class":262,"line":379},[260,1056,1057],{"class":270},"  fonts: ",[260,1059,335],{"class":274},[260,1061,278],{"class":270},[260,1063,1064],{"class":262,"line":391},[260,1065,353],{"emptyLinePlaceholder":352},[260,1067,1068],{"class":262,"line":397},[260,1069,1070],{"class":323},"  // Process only styles from external \u003Clink> tags;\n",[260,1072,1073],{"class":262,"line":430},[260,1074,1075],{"class":323},"  // skip SSR inline styles (e.g. Nuxt UI #nuxt-ui-colors)\n",[260,1077,1078,1081,1083],{"class":262,"line":436},[260,1079,1080],{"class":270},"  reduceInlineStyles: ",[260,1082,335],{"class":274},[260,1084,278],{"class":270},[260,1086,1087],{"class":262,"line":747},[260,1088,1089],{"class":270},"};\n",[81,1091,1093,1094],{"id":1092},"_4-hydration-and-data-beasties-container","4. Hydration and ",[30,1095,74],{},[15,1097,1098,1099,1101,1102,1108],{},"Beasties can use ",[30,1100,74],{}," to narrow the scope of critical CSS evaluation. The docs recommend placing it on ",[19,1103,1104,1105],{},"the outermost wrapper around above-the-fold content inside ",[30,1106,1107],{},"\u003Cbody>",", so Beasties matches selectors against that subtree only — useful for large or deeply nested pages at build time.",[15,1110,1111,1112,1115,1116,1118],{},"If the attribute is missing, Beasties falls back to ",[30,1113,1114],{},"\u003Chtml>"," and writes ",[30,1117,74],{}," itself — effectively evaluating the whole document.",[15,1120,1121,1122,1124,1125,1128,1129,976],{},"This project sets it on ",[30,1123,1114],{}," in ",[30,1126,1127],{},"app/app.vue",". The main goal is not scoping, but ",[19,1130,1131],{},"hydration alignment",[24,1133,1134,1137],{},[27,1135,1136],{},"After Beasties processes prerender HTML, the attribute remains in the output",[27,1138,1139],{},"If the SSR template lacks the same attribute, client hydration may see a DOM mismatch",[15,1141,1142,1143,1145,1146,1148,1149,483],{},"If pages grow and build-time Beasties gets slow, a better optimization is moving ",[30,1144,74],{}," to an above-the-fold wrapper inside ",[30,1147,1107],{}," (e.g. the outer div around hero + nav), rather than keeping it on ",[30,1150,1114],{},[15,1152,1153,1154,1157,1158,976],{},"Relevant ",[30,1155,1156],{},"useHead"," snippet in ",[19,1159,1160],{},[30,1161,1127],{},[251,1163,1167],{"className":1164,"code":1165,"language":1166,"meta":256,"style":256},"language-vue shiki shiki-themes github-light github-light github-dark","\u003Cscript setup lang=\"ts\">\n  useHead(() => ({\n    htmlAttrs: {\n      // Beasties writes this during build-time prerender; SSR output must match.\n      \"data-beasties-container\": \"\",\n    },\n  }));\n\u003C/script>\n","vue",[30,1168,1169,1192,1205,1210,1215,1227,1231,1236],{"__ignoreMap":256},[260,1170,1171,1173,1177,1180,1183,1186,1189],{"class":262,"line":263},[260,1172,540],{"class":270},[260,1174,1176],{"class":1175},"sovSZ","script",[260,1178,1179],{"class":266}," setup",[260,1181,1182],{"class":266}," lang",[260,1184,1185],{"class":270},"=",[260,1187,1188],{"class":311},"\"ts\"",[260,1190,1191],{"class":270},">\n",[260,1193,1194,1197,1200,1202],{"class":262,"line":281},[260,1195,1196],{"class":266},"  useHead",[260,1198,1199],{"class":270},"(() ",[260,1201,588],{"class":292},[260,1203,1204],{"class":270}," ({\n",[260,1206,1207],{"class":262,"line":289},[260,1208,1209],{"class":270},"    htmlAttrs: {\n",[260,1211,1212],{"class":262,"line":299},[260,1213,1214],{"class":323},"      // Beasties writes this during build-time prerender; SSR output must match.\n",[260,1216,1217,1220,1222,1225],{"class":262,"line":308},[260,1218,1219],{"class":311},"      \"data-beasties-container\"",[260,1221,271],{"class":270},[260,1223,1224],{"class":311},"\"\"",[260,1226,278],{"class":270},[260,1228,1229],{"class":262,"line":327},[260,1230,346],{"class":270},[260,1232,1233],{"class":262,"line":343},[260,1234,1235],{"class":270},"  }));\n",[260,1237,1238,1241,1243],{"class":262,"line":349},[260,1239,1240],{"class":270},"\u003C/",[260,1242,1176],{"class":1175},[260,1244,1191],{"class":270},[10,1246,1248],{"id":1247},"summary","📋 Summary",[15,1250,1251,1252,1255,1256,483],{},"With ",[19,1253,1254],{},"SSR on + full prerender of public pages",", the hard part of inlining critical CSS is not \"can we use Beasties?\" but ",[19,1257,1258],{},"when, and on which HTML",[15,1260,1261],{},[19,1262,1263],{},"Two approaches that fail:",[24,1265,1266,1279],{},[27,1267,1268,1272,1273,1275,1276,1278],{},[19,1269,1270,50],{},[30,1271,49],{}," — mutates ",[30,1274,110],{}," in the SSR response chain, outside unhead's pipeline; clashes with Nuxt UI inline styles (e.g. ",[30,1277,123],{},"); high hydration risk.",[27,1280,1281,1285],{},[19,1282,1283],{},[30,1284,58],{}," — Vite build runs before Nitro prerender; the plugin only handles Vite entry HTML, not per-route prerender output.",[15,1287,1288,1291,1292,1296],{},[19,1289,1290],{},"What works:"," run Beasties in ",[19,1293,1294],{},[30,1295,67],{}," on prerendered HTML on disk — module skipped in dev, SSR hot path untouched in prod. Clear split: SSR / prerender produces correct pages; Beasties only accelerates first paint on deployment artifacts.",[15,1298,1299,1302,1303,1306,1307,1310,1311,1313,1314,1316,1317,483],{},[19,1300,1301],{},"Verify:"," after ",[30,1304,1305],{},"npm run build",", inspect ",[30,1308,1309],{},".output/public/index.html"," for inline ",[30,1312,102],{}," plus retained external stylesheet ",[30,1315,106],{}," tags; build logs should show ",[30,1318,1319],{},"Beasties processed N prerendered HTML file(s)",[15,1321,1322,1325],{},[19,1323,1324],{},"Scope:"," Beasties only reduces CSS blocking first paint. Image LCP, deferred JS hydration, API caching, and similar concerns still need separate optimization layers.",[1327,1328,1329],"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":256,"searchDepth":281,"depth":281,"links":1331},[1332,1333,1339,1341,1348],{"id":12,"depth":281,"text":13},{"id":78,"depth":281,"text":79,"children":1334},[1335,1337],{"id":83,"depth":289,"text":1336},"1. render:response + Beasties",{"id":130,"depth":289,"text":1338},"2. vite-plugin-beasties",{"id":146,"depth":281,"text":1340},"✅ Why nitro:build:public-assets + Beasties Works",{"id":218,"depth":281,"text":219,"children":1342},[1343,1344,1345,1346],{"id":235,"depth":289,"text":236},{"id":442,"depth":289,"text":443},{"id":964,"depth":289,"text":965},{"id":1092,"depth":289,"text":1347},"4. Hydration and data-beasties-container",{"id":1247,"depth":281,"text":1248},"architecture","2026-06-15T00:00:00.000Z","Nuxt 4 with SSR enabled, full prerender of public pages, and inline critical CSS:\nrender:response + Beasties → unhead / Nuxt UI inline style conflicts ❌\nvite-plugin-beasties → only processes Vite entry HTML, not per-route prerender output ❌\nnitro:build:public-assets + Beasties ✅",false,"md","en",{"tags":1356},[1357,1358,1359,1360,1361],"nuxt","prerender","beasties","critical-css","frontend-architecture","/blog/en/2026-06-15-prerender-beasties",{"title":5,"description":1351},{"loc":1362},null,"blog/en/2026-06-15-prerender-beasties","2026-06-21T00:00:00.000Z","ffwSr-m39joc1G3URP_gU5CQ-K3ZqbujED-NpxY_0EQ",1784966030636]