[{"data":1,"prerenderedAt":2346},["ShallowReactive",2],{"page-\u002Fvue\u002F20-ssr-and-nuxt-basics":3},{"id":4,"title":5,"body":6,"description":2339,"extension":2340,"meta":2341,"navigation":83,"path":2342,"seo":2343,"stem":2344,"__hash__":2345},"content\u002Fvue\u002F20-ssr-and-nuxt-basics.md","Vue 3 Engineering Reference — SSR & Nuxt Basics",{"type":7,"value":8,"toc":2328},"minimark",[9,14,19,156,160,392,396,797,801,1112,1116,1417,1421,1799,1803,1907,1911,2034,2038,2042,2153,2324],[10,11,13],"h1",{"id":12},"_20-ssr-nuxt-basics","20 — SSR & Nuxt Basics",[15,16,18],"h2",{"id":17},"ssr-lifecycle-server-vs-client","SSR Lifecycle — Server vs Client",[20,21,24],"code-wrapper",{"filename":22,"language":23},"ssr-lifecycle.ts","typescript",[25,26,30],"pre",{"className":27,"code":28,"language":23,"meta":29,"style":29},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F ── Server-side rendering lifecycle: ────────────────────\n\u002F\u002F 1. createApp() — app instance created (per-request in production)\n\u002F\u002F 2. setup() runs for all components — refs, computed, watchers registered\n\u002F\u002F 3. onServerPrefetch callbacks fire — async data fetching\n\u002F\u002F 4. Server waits for all onServerPrefetch promises to resolve\n\u002F\u002F 5. renderToString() — renders the component tree to HTML string\n\u002F\u002F 6. HTML + serialized state sent to client\n\n\u002F\u002F ── Client hydration lifecycle: ──────────────────────────\n\u002F\u002F 1. Browser receives HTML + JS bundle\n\u002F\u002F 2. HTML is displayed immediately (user sees content before JS loads)\n\u002F\u002F 3. createApp() — same component tree created on client\n\u002F\u002F 4. onServerPrefetch SKIPPED — server already fetched the data\n\u002F\u002F 5. Hydration: Vue \"attaches\" to existing DOM (no re-render, just event listeners)\n\u002F\u002F 6. If DOM matches expected VNodes: seamless. If not: hydration mismatch warning.\n\n\u002F\u002F ── Key difference: hydration is NOT a re-render ─────────\n\u002F\u002F Vue assumes the server HTML is correct and just adds interactivity.\n\u002F\u002F If the client and server produce different HTML, Vue discards the server\n\u002F\u002F HTML and re-renders from scratch (expensive, visible flash).\n","",[31,32,33,42,48,54,60,66,72,78,85,91,97,103,109,115,121,127,132,138,144,150],"code",{"__ignoreMap":29},[34,35,38],"span",{"class":36,"line":37},"line",1,[34,39,41],{"class":40},"sdCPZ","\u002F\u002F ── Server-side rendering lifecycle: ────────────────────\n",[34,43,45],{"class":36,"line":44},2,[34,46,47],{"class":40},"\u002F\u002F 1. createApp() — app instance created (per-request in production)\n",[34,49,51],{"class":36,"line":50},3,[34,52,53],{"class":40},"\u002F\u002F 2. setup() runs for all components — refs, computed, watchers registered\n",[34,55,57],{"class":36,"line":56},4,[34,58,59],{"class":40},"\u002F\u002F 3. onServerPrefetch callbacks fire — async data fetching\n",[34,61,63],{"class":36,"line":62},5,[34,64,65],{"class":40},"\u002F\u002F 4. Server waits for all onServerPrefetch promises to resolve\n",[34,67,69],{"class":36,"line":68},6,[34,70,71],{"class":40},"\u002F\u002F 5. renderToString() — renders the component tree to HTML string\n",[34,73,75],{"class":36,"line":74},7,[34,76,77],{"class":40},"\u002F\u002F 6. HTML + serialized state sent to client\n",[34,79,81],{"class":36,"line":80},8,[34,82,84],{"emptyLinePlaceholder":83},true,"\n",[34,86,88],{"class":36,"line":87},9,[34,89,90],{"class":40},"\u002F\u002F ── Client hydration lifecycle: ──────────────────────────\n",[34,92,94],{"class":36,"line":93},10,[34,95,96],{"class":40},"\u002F\u002F 1. Browser receives HTML + JS bundle\n",[34,98,100],{"class":36,"line":99},11,[34,101,102],{"class":40},"\u002F\u002F 2. HTML is displayed immediately (user sees content before JS loads)\n",[34,104,106],{"class":36,"line":105},12,[34,107,108],{"class":40},"\u002F\u002F 3. createApp() — same component tree created on client\n",[34,110,112],{"class":36,"line":111},13,[34,113,114],{"class":40},"\u002F\u002F 4. onServerPrefetch SKIPPED — server already fetched the data\n",[34,116,118],{"class":36,"line":117},14,[34,119,120],{"class":40},"\u002F\u002F 5. Hydration: Vue \"attaches\" to existing DOM (no re-render, just event listeners)\n",[34,122,124],{"class":36,"line":123},15,[34,125,126],{"class":40},"\u002F\u002F 6. If DOM matches expected VNodes: seamless. If not: hydration mismatch warning.\n",[34,128,130],{"class":36,"line":129},16,[34,131,84],{"emptyLinePlaceholder":83},[34,133,135],{"class":36,"line":134},17,[34,136,137],{"class":40},"\u002F\u002F ── Key difference: hydration is NOT a re-render ─────────\n",[34,139,141],{"class":36,"line":140},18,[34,142,143],{"class":40},"\u002F\u002F Vue assumes the server HTML is correct and just adds interactivity.\n",[34,145,147],{"class":36,"line":146},19,[34,148,149],{"class":40},"\u002F\u002F If the client and server produce different HTML, Vue discards the server\n",[34,151,153],{"class":36,"line":152},20,[34,154,155],{"class":40},"\u002F\u002F HTML and re-renders from scratch (expensive, visible flash).\n",[15,157,159],{"id":158},"onserverprefetch-data-preloading-on-server","onServerPrefetch — Data Preloading on Server",[20,161,163],{"filename":162,"language":23},"SSRDataFetch.ts",[25,164,166],{"className":27,"code":165,"language":23,"meta":29,"style":29},"import { ref, onServerPrefetch, onMounted } from 'vue'\n\nconst data = ref(null)\n\n\u002F\u002F ── onServerPrefetch: runs ONLY on the server during SSR ──\n\u002F\u002F The callback returns a promise — server waits for resolution before rendering.\n\u002F\u002F The fetched data is in the component's reactive state when HTML is generated.\nonServerPrefetch(async () => {\n  const res = await fetch('https:\u002F\u002Fapi.example.com\u002Fdata')\n  data.value = await res.json()\n})\n\n\u002F\u002F ── Client-side: fetch if data wasn't provided by SSR (no SSR data) ──\n\u002F\u002F During hydration, onServerPrefetch is skipped — data stays null if not injected.\n\u002F\u002F This onMounted acts as a fallback for client-side navigation (SPA mode).\nonMounted(async () => {\n  if (!data.value) {\n    const res = await fetch('\u002Fapi\u002Fdata')\n    data.value = await res.json()\n  }\n})\n",[31,167,168,185,189,214,218,223,228,233,252,275,294,299,303,308,313,318,333,347,367,382,387],{"__ignoreMap":29},[34,169,170,174,178,181],{"class":36,"line":37},[34,171,173],{"class":172},"svdQ7","import",[34,175,177],{"class":176},"ssxIu"," { ref, onServerPrefetch, onMounted } ",[34,179,180],{"class":172},"from",[34,182,184],{"class":183},"sJ6F3"," 'vue'\n",[34,186,187],{"class":36,"line":44},[34,188,84],{"emptyLinePlaceholder":83},[34,190,191,194,198,201,205,208,211],{"class":36,"line":50},[34,192,193],{"class":172},"const",[34,195,197],{"class":196},"snvgF"," data",[34,199,200],{"class":172}," =",[34,202,204],{"class":203},"sIsaT"," ref",[34,206,207],{"class":176},"(",[34,209,210],{"class":196},"null",[34,212,213],{"class":176},")\n",[34,215,216],{"class":36,"line":56},[34,217,84],{"emptyLinePlaceholder":83},[34,219,220],{"class":36,"line":62},[34,221,222],{"class":40},"\u002F\u002F ── onServerPrefetch: runs ONLY on the server during SSR ──\n",[34,224,225],{"class":36,"line":68},[34,226,227],{"class":40},"\u002F\u002F The callback returns a promise — server waits for resolution before rendering.\n",[34,229,230],{"class":36,"line":74},[34,231,232],{"class":40},"\u002F\u002F The fetched data is in the component's reactive state when HTML is generated.\n",[34,234,235,238,240,243,246,249],{"class":36,"line":80},[34,236,237],{"class":203},"onServerPrefetch",[34,239,207],{"class":176},[34,241,242],{"class":172},"async",[34,244,245],{"class":176}," () ",[34,247,248],{"class":172},"=>",[34,250,251],{"class":176}," {\n",[34,253,254,257,260,262,265,268,270,273],{"class":36,"line":87},[34,255,256],{"class":172},"  const",[34,258,259],{"class":196}," res",[34,261,200],{"class":172},[34,263,264],{"class":172}," await",[34,266,267],{"class":203}," fetch",[34,269,207],{"class":176},[34,271,272],{"class":183},"'https:\u002F\u002Fapi.example.com\u002Fdata'",[34,274,213],{"class":176},[34,276,277,280,283,285,288,291],{"class":36,"line":93},[34,278,279],{"class":176},"  data.value ",[34,281,282],{"class":172},"=",[34,284,264],{"class":172},[34,286,287],{"class":176}," res.",[34,289,290],{"class":203},"json",[34,292,293],{"class":176},"()\n",[34,295,296],{"class":36,"line":99},[34,297,298],{"class":176},"})\n",[34,300,301],{"class":36,"line":105},[34,302,84],{"emptyLinePlaceholder":83},[34,304,305],{"class":36,"line":111},[34,306,307],{"class":40},"\u002F\u002F ── Client-side: fetch if data wasn't provided by SSR (no SSR data) ──\n",[34,309,310],{"class":36,"line":117},[34,311,312],{"class":40},"\u002F\u002F During hydration, onServerPrefetch is skipped — data stays null if not injected.\n",[34,314,315],{"class":36,"line":123},[34,316,317],{"class":40},"\u002F\u002F This onMounted acts as a fallback for client-side navigation (SPA mode).\n",[34,319,320,323,325,327,329,331],{"class":36,"line":129},[34,321,322],{"class":203},"onMounted",[34,324,207],{"class":176},[34,326,242],{"class":172},[34,328,245],{"class":176},[34,330,248],{"class":172},[34,332,251],{"class":176},[34,334,335,338,341,344],{"class":36,"line":134},[34,336,337],{"class":172},"  if",[34,339,340],{"class":176}," (",[34,342,343],{"class":172},"!",[34,345,346],{"class":176},"data.value) {\n",[34,348,349,352,354,356,358,360,362,365],{"class":36,"line":140},[34,350,351],{"class":172},"    const",[34,353,259],{"class":196},[34,355,200],{"class":172},[34,357,264],{"class":172},[34,359,267],{"class":203},[34,361,207],{"class":176},[34,363,364],{"class":183},"'\u002Fapi\u002Fdata'",[34,366,213],{"class":176},[34,368,369,372,374,376,378,380],{"class":36,"line":146},[34,370,371],{"class":176},"    data.value ",[34,373,282],{"class":172},[34,375,264],{"class":172},[34,377,287],{"class":176},[34,379,290],{"class":203},[34,381,293],{"class":176},[34,383,384],{"class":36,"line":152},[34,385,386],{"class":176},"  }\n",[34,388,390],{"class":36,"line":389},21,[34,391,298],{"class":176},[15,393,395],{"id":394},"state-serialization-server-to-client","State Serialization — Server to Client",[20,397,399],{"filename":398,"language":23},"state-serialization.ts",[25,400,402],{"className":27,"code":401,"language":23,"meta":29,"style":29},"import { createSSRApp } from 'vue'\nimport { renderToString } from '@vue\u002Fserver-renderer'\nimport { createPinia } from 'pinia'\nimport App from '.\u002FApp.vue'\n\n\u002F\u002F ── Server: render + serialize state ─────────────────────\nexport async function serverRender() {\n  const pinia = createPinia()  \u002F\u002F FRESH per request (no cross-request leaks)\n  const app = createSSRApp(App)\n  app.use(pinia)\n\n  \u002F\u002F ── Pre-populate Pinia stores (equivalent to onServerPrefetch) ──\n  const userStore = useUserStore(pinia)\n  await userStore.fetchUser()\n\n  \u002F\u002F ── Render to HTML string ──\n  const html = await renderToString(app)\n\n  \u002F\u002F ── Serialize Pinia state for client hydration ──\n  const state = JSON.stringify(pinia.state.value)\n\n  \u002F\u002F ── Inject into HTML: client reads this on load ──\n  return `\n    \u003Cdiv id=\"app\">${html}\u003C\u002Fdiv>\n    \u003Cscript>window.__PINIA_STATE__ = ${state}\u003C\u002Fscript>\n  `\n}\n\n\u002F\u002F ── Client: hydrate with server state ───────────────────\nexport function clientHydrate() {\n  const pinia = createPinia()\n\n  \u002F\u002F ── Restore server state before mounting ──\n  if (window.__PINIA_STATE__) {\n    pinia.state.value = window.__PINIA_STATE__  \u002F\u002F replace, not merge\n  }\n\n  const app = createSSRApp(App)  \u002F\u002F createSSRApp for hydration (not createApp)\n  app.use(pinia)\n  app.mount('#app')  \u002F\u002F hydrates existing DOM, doesn't re-render\n}\n",[31,403,404,415,427,439,451,455,460,477,495,510,521,525,530,544,557,561,566,583,587,592,613,617,623,632,644,656,662,668,673,679,691,704,709,715,723,737,742,747,764,773,792],{"__ignoreMap":29},[34,405,406,408,411,413],{"class":36,"line":37},[34,407,173],{"class":172},[34,409,410],{"class":176}," { createSSRApp } ",[34,412,180],{"class":172},[34,414,184],{"class":183},[34,416,417,419,422,424],{"class":36,"line":44},[34,418,173],{"class":172},[34,420,421],{"class":176}," { renderToString } ",[34,423,180],{"class":172},[34,425,426],{"class":183}," '@vue\u002Fserver-renderer'\n",[34,428,429,431,434,436],{"class":36,"line":50},[34,430,173],{"class":172},[34,432,433],{"class":176}," { createPinia } ",[34,435,180],{"class":172},[34,437,438],{"class":183}," 'pinia'\n",[34,440,441,443,446,448],{"class":36,"line":56},[34,442,173],{"class":172},[34,444,445],{"class":176}," App ",[34,447,180],{"class":172},[34,449,450],{"class":183}," '.\u002FApp.vue'\n",[34,452,453],{"class":36,"line":62},[34,454,84],{"emptyLinePlaceholder":83},[34,456,457],{"class":36,"line":68},[34,458,459],{"class":40},"\u002F\u002F ── Server: render + serialize state ─────────────────────\n",[34,461,462,465,468,471,474],{"class":36,"line":74},[34,463,464],{"class":172},"export",[34,466,467],{"class":172}," async",[34,469,470],{"class":172}," function",[34,472,473],{"class":203}," serverRender",[34,475,476],{"class":176},"() {\n",[34,478,479,481,484,486,489,492],{"class":36,"line":80},[34,480,256],{"class":172},[34,482,483],{"class":196}," pinia",[34,485,200],{"class":172},[34,487,488],{"class":203}," createPinia",[34,490,491],{"class":176},"()  ",[34,493,494],{"class":40},"\u002F\u002F FRESH per request (no cross-request leaks)\n",[34,496,497,499,502,504,507],{"class":36,"line":87},[34,498,256],{"class":172},[34,500,501],{"class":196}," app",[34,503,200],{"class":172},[34,505,506],{"class":203}," createSSRApp",[34,508,509],{"class":176},"(App)\n",[34,511,512,515,518],{"class":36,"line":93},[34,513,514],{"class":176},"  app.",[34,516,517],{"class":203},"use",[34,519,520],{"class":176},"(pinia)\n",[34,522,523],{"class":36,"line":99},[34,524,84],{"emptyLinePlaceholder":83},[34,526,527],{"class":36,"line":105},[34,528,529],{"class":40},"  \u002F\u002F ── Pre-populate Pinia stores (equivalent to onServerPrefetch) ──\n",[34,531,532,534,537,539,542],{"class":36,"line":111},[34,533,256],{"class":172},[34,535,536],{"class":196}," userStore",[34,538,200],{"class":172},[34,540,541],{"class":203}," useUserStore",[34,543,520],{"class":176},[34,545,546,549,552,555],{"class":36,"line":117},[34,547,548],{"class":172},"  await",[34,550,551],{"class":176}," userStore.",[34,553,554],{"class":203},"fetchUser",[34,556,293],{"class":176},[34,558,559],{"class":36,"line":123},[34,560,84],{"emptyLinePlaceholder":83},[34,562,563],{"class":36,"line":129},[34,564,565],{"class":40},"  \u002F\u002F ── Render to HTML string ──\n",[34,567,568,570,573,575,577,580],{"class":36,"line":134},[34,569,256],{"class":172},[34,571,572],{"class":196}," html",[34,574,200],{"class":172},[34,576,264],{"class":172},[34,578,579],{"class":203}," renderToString",[34,581,582],{"class":176},"(app)\n",[34,584,585],{"class":36,"line":140},[34,586,84],{"emptyLinePlaceholder":83},[34,588,589],{"class":36,"line":146},[34,590,591],{"class":40},"  \u002F\u002F ── Serialize Pinia state for client hydration ──\n",[34,593,594,596,599,601,604,607,610],{"class":36,"line":152},[34,595,256],{"class":172},[34,597,598],{"class":196}," state",[34,600,200],{"class":172},[34,602,603],{"class":196}," JSON",[34,605,606],{"class":176},".",[34,608,609],{"class":203},"stringify",[34,611,612],{"class":176},"(pinia.state.value)\n",[34,614,615],{"class":36,"line":389},[34,616,84],{"emptyLinePlaceholder":83},[34,618,620],{"class":36,"line":619},22,[34,621,622],{"class":40},"  \u002F\u002F ── Inject into HTML: client reads this on load ──\n",[34,624,626,629],{"class":36,"line":625},23,[34,627,628],{"class":172},"  return",[34,630,631],{"class":183}," `\n",[34,633,635,638,641],{"class":36,"line":634},24,[34,636,637],{"class":183},"    \u003Cdiv id=\"app\">${",[34,639,640],{"class":176},"html",[34,642,643],{"class":183},"}\u003C\u002Fdiv>\n",[34,645,647,650,653],{"class":36,"line":646},25,[34,648,649],{"class":183},"    \u003Cscript>window.__PINIA_STATE__ = ${",[34,651,652],{"class":176},"state",[34,654,655],{"class":183},"}\u003C\u002Fscript>\n",[34,657,659],{"class":36,"line":658},26,[34,660,661],{"class":183},"  `\n",[34,663,665],{"class":36,"line":664},27,[34,666,667],{"class":176},"}\n",[34,669,671],{"class":36,"line":670},28,[34,672,84],{"emptyLinePlaceholder":83},[34,674,676],{"class":36,"line":675},29,[34,677,678],{"class":40},"\u002F\u002F ── Client: hydrate with server state ───────────────────\n",[34,680,682,684,686,689],{"class":36,"line":681},30,[34,683,464],{"class":172},[34,685,470],{"class":172},[34,687,688],{"class":203}," clientHydrate",[34,690,476],{"class":176},[34,692,694,696,698,700,702],{"class":36,"line":693},31,[34,695,256],{"class":172},[34,697,483],{"class":196},[34,699,200],{"class":172},[34,701,488],{"class":203},[34,703,293],{"class":176},[34,705,707],{"class":36,"line":706},32,[34,708,84],{"emptyLinePlaceholder":83},[34,710,712],{"class":36,"line":711},33,[34,713,714],{"class":40},"  \u002F\u002F ── Restore server state before mounting ──\n",[34,716,718,720],{"class":36,"line":717},34,[34,719,337],{"class":172},[34,721,722],{"class":176}," (window.__PINIA_STATE__) {\n",[34,724,726,729,731,734],{"class":36,"line":725},35,[34,727,728],{"class":176},"    pinia.state.value ",[34,730,282],{"class":172},[34,732,733],{"class":176}," window.__PINIA_STATE__  ",[34,735,736],{"class":40},"\u002F\u002F replace, not merge\n",[34,738,740],{"class":36,"line":739},36,[34,741,386],{"class":176},[34,743,745],{"class":36,"line":744},37,[34,746,84],{"emptyLinePlaceholder":83},[34,748,750,752,754,756,758,761],{"class":36,"line":749},38,[34,751,256],{"class":172},[34,753,501],{"class":196},[34,755,200],{"class":172},[34,757,506],{"class":203},[34,759,760],{"class":176},"(App)  ",[34,762,763],{"class":40},"\u002F\u002F createSSRApp for hydration (not createApp)\n",[34,765,767,769,771],{"class":36,"line":766},39,[34,768,514],{"class":176},[34,770,517],{"class":203},[34,772,520],{"class":176},[34,774,776,778,781,783,786,789],{"class":36,"line":775},40,[34,777,514],{"class":176},[34,779,780],{"class":203},"mount",[34,782,207],{"class":176},[34,784,785],{"class":183},"'#app'",[34,787,788],{"class":176},")  ",[34,790,791],{"class":40},"\u002F\u002F hydrates existing DOM, doesn't re-render\n",[34,793,795],{"class":36,"line":794},41,[34,796,667],{"class":176},[15,798,800],{"id":799},"hydration-mismatches-causes-and-fixes","Hydration Mismatches — Causes and Fixes",[20,802,804],{"filename":803,"language":23},"hydration-mismatches.ts",[25,805,807],{"className":27,"code":806,"language":23,"meta":29,"style":29},"import { ref, computed, onMounted } from 'vue'\n\n\u002F\u002F ── 1. Date\u002Ftime-based rendering (server time ≠ client time) ──\nconst now = ref(new Date())\n\u002F\u002F Server renders 14:00, client renders 14:01 → mismatch\n\u002F\u002F Fix: render relative time on client only, or use a fixed timestamp from server\n\nconst formattedTime = computed(() => {\n  \u002F\u002F ❌ Bad: server and client compute different values\n  return new Date().toLocaleTimeString()  \u002F\u002F different on server vs client\n\n  \u002F\u002F ✅ Good: use a fixed timestamp (from server data)\n  return new Date(serverTimestamp.value).toLocaleTimeString()\n})\n\n\u002F\u002F ── 2. Math.random() in render ──────────────────────────\nconst randomId = Math.random()  \u002F\u002F different on server and client\n\u002F\u002F Fix: generate random IDs in onMounted (client-only) or use UUID from server\n\n\u002F\u002F ── 3. window\u002Fdocument in template or setup ─────────────\n\u002F\u002F Server: no window → undefined or error\nconst width = ref(0)\n\u002F\u002F ❌ Bad: window in setup — crashes on server\n\u002F\u002F const width = ref(window.innerWidth)\n\u002F\u002F ✅ Good: read in onMounted (client-only)\nonMounted(() => { width.value = window.innerWidth })\n\n\u002F\u002F ── 4. v-if based on client-only state ───────────────────\nconst isClient = ref(false)\nonMounted(() => { isClient.value = true })  \u002F\u002F true only on client\n\u002F\u002F Server renders v-if=\"false\" (hidden), client renders v-if=\"true\" (shown) → mismatch\n\u002F\u002F Fix: use ClientOnly wrapper (Nuxt) or :disabled on Teleport\n\n\u002F\u002F ── 5. CSS order differences (server vs client bundle) ──\n\u002F\u002F Different CSS load order can cause different computed styles.\n\u002F\u002F Not a Vue issue per se, but manifests as hydration warning.\n",[31,808,809,820,824,829,851,856,861,865,884,889,909,913,918,933,937,941,946,966,971,975,980,985,1003,1008,1013,1018,1034,1038,1043,1061,1083,1088,1093,1097,1102,1107],{"__ignoreMap":29},[34,810,811,813,816,818],{"class":36,"line":37},[34,812,173],{"class":172},[34,814,815],{"class":176}," { ref, computed, onMounted } ",[34,817,180],{"class":172},[34,819,184],{"class":183},[34,821,822],{"class":36,"line":44},[34,823,84],{"emptyLinePlaceholder":83},[34,825,826],{"class":36,"line":50},[34,827,828],{"class":40},"\u002F\u002F ── 1. Date\u002Ftime-based rendering (server time ≠ client time) ──\n",[34,830,831,833,836,838,840,842,845,848],{"class":36,"line":56},[34,832,193],{"class":172},[34,834,835],{"class":196}," now",[34,837,200],{"class":172},[34,839,204],{"class":203},[34,841,207],{"class":176},[34,843,844],{"class":172},"new",[34,846,847],{"class":203}," Date",[34,849,850],{"class":176},"())\n",[34,852,853],{"class":36,"line":62},[34,854,855],{"class":40},"\u002F\u002F Server renders 14:00, client renders 14:01 → mismatch\n",[34,857,858],{"class":36,"line":68},[34,859,860],{"class":40},"\u002F\u002F Fix: render relative time on client only, or use a fixed timestamp from server\n",[34,862,863],{"class":36,"line":74},[34,864,84],{"emptyLinePlaceholder":83},[34,866,867,869,872,874,877,880,882],{"class":36,"line":80},[34,868,193],{"class":172},[34,870,871],{"class":196}," formattedTime",[34,873,200],{"class":172},[34,875,876],{"class":203}," computed",[34,878,879],{"class":176},"(() ",[34,881,248],{"class":172},[34,883,251],{"class":176},[34,885,886],{"class":36,"line":87},[34,887,888],{"class":40},"  \u002F\u002F ❌ Bad: server and client compute different values\n",[34,890,891,893,896,898,901,904,906],{"class":36,"line":93},[34,892,628],{"class":172},[34,894,895],{"class":172}," new",[34,897,847],{"class":203},[34,899,900],{"class":176},"().",[34,902,903],{"class":203},"toLocaleTimeString",[34,905,491],{"class":176},[34,907,908],{"class":40},"\u002F\u002F different on server vs client\n",[34,910,911],{"class":36,"line":99},[34,912,84],{"emptyLinePlaceholder":83},[34,914,915],{"class":36,"line":105},[34,916,917],{"class":40},"  \u002F\u002F ✅ Good: use a fixed timestamp (from server data)\n",[34,919,920,922,924,926,929,931],{"class":36,"line":111},[34,921,628],{"class":172},[34,923,895],{"class":172},[34,925,847],{"class":203},[34,927,928],{"class":176},"(serverTimestamp.value).",[34,930,903],{"class":203},[34,932,293],{"class":176},[34,934,935],{"class":36,"line":117},[34,936,298],{"class":176},[34,938,939],{"class":36,"line":123},[34,940,84],{"emptyLinePlaceholder":83},[34,942,943],{"class":36,"line":129},[34,944,945],{"class":40},"\u002F\u002F ── 2. Math.random() in render ──────────────────────────\n",[34,947,948,950,953,955,958,961,963],{"class":36,"line":134},[34,949,193],{"class":172},[34,951,952],{"class":196}," randomId",[34,954,200],{"class":172},[34,956,957],{"class":176}," Math.",[34,959,960],{"class":203},"random",[34,962,491],{"class":176},[34,964,965],{"class":40},"\u002F\u002F different on server and client\n",[34,967,968],{"class":36,"line":140},[34,969,970],{"class":40},"\u002F\u002F Fix: generate random IDs in onMounted (client-only) or use UUID from server\n",[34,972,973],{"class":36,"line":146},[34,974,84],{"emptyLinePlaceholder":83},[34,976,977],{"class":36,"line":152},[34,978,979],{"class":40},"\u002F\u002F ── 3. window\u002Fdocument in template or setup ─────────────\n",[34,981,982],{"class":36,"line":389},[34,983,984],{"class":40},"\u002F\u002F Server: no window → undefined or error\n",[34,986,987,989,992,994,996,998,1001],{"class":36,"line":619},[34,988,193],{"class":172},[34,990,991],{"class":196}," width",[34,993,200],{"class":172},[34,995,204],{"class":203},[34,997,207],{"class":176},[34,999,1000],{"class":196},"0",[34,1002,213],{"class":176},[34,1004,1005],{"class":36,"line":625},[34,1006,1007],{"class":40},"\u002F\u002F ❌ Bad: window in setup — crashes on server\n",[34,1009,1010],{"class":36,"line":634},[34,1011,1012],{"class":40},"\u002F\u002F const width = ref(window.innerWidth)\n",[34,1014,1015],{"class":36,"line":646},[34,1016,1017],{"class":40},"\u002F\u002F ✅ Good: read in onMounted (client-only)\n",[34,1019,1020,1022,1024,1026,1029,1031],{"class":36,"line":658},[34,1021,322],{"class":203},[34,1023,879],{"class":176},[34,1025,248],{"class":172},[34,1027,1028],{"class":176}," { width.value ",[34,1030,282],{"class":172},[34,1032,1033],{"class":176}," window.innerWidth })\n",[34,1035,1036],{"class":36,"line":664},[34,1037,84],{"emptyLinePlaceholder":83},[34,1039,1040],{"class":36,"line":670},[34,1041,1042],{"class":40},"\u002F\u002F ── 4. v-if based on client-only state ───────────────────\n",[34,1044,1045,1047,1050,1052,1054,1056,1059],{"class":36,"line":675},[34,1046,193],{"class":172},[34,1048,1049],{"class":196}," isClient",[34,1051,200],{"class":172},[34,1053,204],{"class":203},[34,1055,207],{"class":176},[34,1057,1058],{"class":196},"false",[34,1060,213],{"class":176},[34,1062,1063,1065,1067,1069,1072,1074,1077,1080],{"class":36,"line":681},[34,1064,322],{"class":203},[34,1066,879],{"class":176},[34,1068,248],{"class":172},[34,1070,1071],{"class":176}," { isClient.value ",[34,1073,282],{"class":172},[34,1075,1076],{"class":196}," true",[34,1078,1079],{"class":176}," })  ",[34,1081,1082],{"class":40},"\u002F\u002F true only on client\n",[34,1084,1085],{"class":36,"line":693},[34,1086,1087],{"class":40},"\u002F\u002F Server renders v-if=\"false\" (hidden), client renders v-if=\"true\" (shown) → mismatch\n",[34,1089,1090],{"class":36,"line":706},[34,1091,1092],{"class":40},"\u002F\u002F Fix: use ClientOnly wrapper (Nuxt) or :disabled on Teleport\n",[34,1094,1095],{"class":36,"line":711},[34,1096,84],{"emptyLinePlaceholder":83},[34,1098,1099],{"class":36,"line":717},[34,1100,1101],{"class":40},"\u002F\u002F ── 5. CSS order differences (server vs client bundle) ──\n",[34,1103,1104],{"class":36,"line":725},[34,1105,1106],{"class":40},"\u002F\u002F Different CSS load order can cause different computed styles.\n",[34,1108,1109],{"class":36,"line":739},[34,1110,1111],{"class":40},"\u002F\u002F Not a Vue issue per se, but manifests as hydration warning.\n",[15,1113,1115],{"id":1114},"nuxt-universal-rendering-architecture","Nuxt — Universal Rendering Architecture",[20,1117,1119],{"filename":1118,"language":23},"nuxt-patterns.ts",[25,1120,1122],{"className":27,"code":1121,"language":23,"meta":29,"style":29},"\u002F\u002F ── Nuxt 3: file-based routing, auto-imports, universal rendering ──\n\n\u002F\u002F ── 1. useFetch: Nuxt's built-in data fetching (SSR + client) ──\nimport { useFetch, useAsyncData } from '#imports'  \u002F\u002F auto-imported\n\n\u002F\u002F useFetch: handles SSR pre-fetch, client hydration, and client-side refetch\nconst { data: users, pending, error, refresh } = await useFetch('\u002Fapi\u002Fusers')\n\u002F\u002F Server: fetches during SSR, serializes result in payload\n\u002F\u002F Client: hydrates from payload (no re-fetch), refetches on client navigation\n\n\u002F\u002F ── 2. useAsyncData: custom fetcher with SSR ──\nconst { data } = await useAsyncData('users', async () => {\n  \u002F\u002F Custom fetcher — Nuxt handles SSR serialization + hydration\n  const res = await $fetch('\u002Fapi\u002Fusers')\n  return res\n})\n\n\u002F\u002F ── 3. Server routes (Nuxt server API) ──\n\u002F\u002F server\u002Fapi\u002Fusers.get.ts:\n\u002F\u002F export default defineEventHandler(async () => {\n\u002F\u002F   return await db.query('SELECT * FROM users')\n\u002F\u002F })\n\n\u002F\u002F ── 4. ClientOnly: render children only on client (avoid mismatch) ──\n\u002F\u002F \u003CClientOnly>\n\u002F\u002F   \u003CMapComponent \u002F>  \u003C!-- rendered only after hydration -->\n\u002F\u002F   \u003Ctemplate #fallback>\n\u002F\u002F     \u003Cdiv>Loading map…\u003C\u002Fdiv>  \u003C!-- server + initial client -->\n\u002F\u002F   \u003C\u002Ftemplate>\n\u002F\u002F \u003C\u002FClientOnly>\n\n\u002F\u002F ── 5. SSR-safe window access: import.meta.client ──\nif (import.meta.client) {\n  \u002F\u002F Client-only code — safely skipped on server\n  window.addEventListener('resize', handler)\n}\n",[31,1123,1124,1129,1133,1138,1153,1157,1162,1212,1217,1222,1226,1231,1263,1268,1287,1294,1298,1302,1307,1312,1317,1322,1327,1331,1336,1341,1346,1351,1356,1361,1366,1370,1375,1392,1397,1413],{"__ignoreMap":29},[34,1125,1126],{"class":36,"line":37},[34,1127,1128],{"class":40},"\u002F\u002F ── Nuxt 3: file-based routing, auto-imports, universal rendering ──\n",[34,1130,1131],{"class":36,"line":44},[34,1132,84],{"emptyLinePlaceholder":83},[34,1134,1135],{"class":36,"line":50},[34,1136,1137],{"class":40},"\u002F\u002F ── 1. useFetch: Nuxt's built-in data fetching (SSR + client) ──\n",[34,1139,1140,1142,1145,1147,1150],{"class":36,"line":56},[34,1141,173],{"class":172},[34,1143,1144],{"class":176}," { useFetch, useAsyncData } ",[34,1146,180],{"class":172},[34,1148,1149],{"class":183}," '#imports'",[34,1151,1152],{"class":40},"  \u002F\u002F auto-imported\n",[34,1154,1155],{"class":36,"line":62},[34,1156,84],{"emptyLinePlaceholder":83},[34,1158,1159],{"class":36,"line":68},[34,1160,1161],{"class":40},"\u002F\u002F useFetch: handles SSR pre-fetch, client hydration, and client-side refetch\n",[34,1163,1164,1166,1169,1173,1176,1179,1182,1185,1187,1190,1192,1195,1198,1200,1202,1205,1207,1210],{"class":36,"line":74},[34,1165,193],{"class":172},[34,1167,1168],{"class":176}," { ",[34,1170,1172],{"class":1171},"sCrzJ","data",[34,1174,1175],{"class":176},": ",[34,1177,1178],{"class":196},"users",[34,1180,1181],{"class":176},", ",[34,1183,1184],{"class":196},"pending",[34,1186,1181],{"class":176},[34,1188,1189],{"class":196},"error",[34,1191,1181],{"class":176},[34,1193,1194],{"class":196},"refresh",[34,1196,1197],{"class":176}," } ",[34,1199,282],{"class":172},[34,1201,264],{"class":172},[34,1203,1204],{"class":203}," useFetch",[34,1206,207],{"class":176},[34,1208,1209],{"class":183},"'\u002Fapi\u002Fusers'",[34,1211,213],{"class":176},[34,1213,1214],{"class":36,"line":80},[34,1215,1216],{"class":40},"\u002F\u002F Server: fetches during SSR, serializes result in payload\n",[34,1218,1219],{"class":36,"line":87},[34,1220,1221],{"class":40},"\u002F\u002F Client: hydrates from payload (no re-fetch), refetches on client navigation\n",[34,1223,1224],{"class":36,"line":93},[34,1225,84],{"emptyLinePlaceholder":83},[34,1227,1228],{"class":36,"line":99},[34,1229,1230],{"class":40},"\u002F\u002F ── 2. useAsyncData: custom fetcher with SSR ──\n",[34,1232,1233,1235,1237,1239,1241,1243,1245,1248,1250,1253,1255,1257,1259,1261],{"class":36,"line":105},[34,1234,193],{"class":172},[34,1236,1168],{"class":176},[34,1238,1172],{"class":196},[34,1240,1197],{"class":176},[34,1242,282],{"class":172},[34,1244,264],{"class":172},[34,1246,1247],{"class":203}," useAsyncData",[34,1249,207],{"class":176},[34,1251,1252],{"class":183},"'users'",[34,1254,1181],{"class":176},[34,1256,242],{"class":172},[34,1258,245],{"class":176},[34,1260,248],{"class":172},[34,1262,251],{"class":176},[34,1264,1265],{"class":36,"line":111},[34,1266,1267],{"class":40},"  \u002F\u002F Custom fetcher — Nuxt handles SSR serialization + hydration\n",[34,1269,1270,1272,1274,1276,1278,1281,1283,1285],{"class":36,"line":117},[34,1271,256],{"class":172},[34,1273,259],{"class":196},[34,1275,200],{"class":172},[34,1277,264],{"class":172},[34,1279,1280],{"class":203}," $fetch",[34,1282,207],{"class":176},[34,1284,1209],{"class":183},[34,1286,213],{"class":176},[34,1288,1289,1291],{"class":36,"line":123},[34,1290,628],{"class":172},[34,1292,1293],{"class":176}," res\n",[34,1295,1296],{"class":36,"line":129},[34,1297,298],{"class":176},[34,1299,1300],{"class":36,"line":134},[34,1301,84],{"emptyLinePlaceholder":83},[34,1303,1304],{"class":36,"line":140},[34,1305,1306],{"class":40},"\u002F\u002F ── 3. Server routes (Nuxt server API) ──\n",[34,1308,1309],{"class":36,"line":146},[34,1310,1311],{"class":40},"\u002F\u002F server\u002Fapi\u002Fusers.get.ts:\n",[34,1313,1314],{"class":36,"line":152},[34,1315,1316],{"class":40},"\u002F\u002F export default defineEventHandler(async () => {\n",[34,1318,1319],{"class":36,"line":389},[34,1320,1321],{"class":40},"\u002F\u002F   return await db.query('SELECT * FROM users')\n",[34,1323,1324],{"class":36,"line":619},[34,1325,1326],{"class":40},"\u002F\u002F })\n",[34,1328,1329],{"class":36,"line":625},[34,1330,84],{"emptyLinePlaceholder":83},[34,1332,1333],{"class":36,"line":634},[34,1334,1335],{"class":40},"\u002F\u002F ── 4. ClientOnly: render children only on client (avoid mismatch) ──\n",[34,1337,1338],{"class":36,"line":646},[34,1339,1340],{"class":40},"\u002F\u002F \u003CClientOnly>\n",[34,1342,1343],{"class":36,"line":658},[34,1344,1345],{"class":40},"\u002F\u002F   \u003CMapComponent \u002F>  \u003C!-- rendered only after hydration -->\n",[34,1347,1348],{"class":36,"line":664},[34,1349,1350],{"class":40},"\u002F\u002F   \u003Ctemplate #fallback>\n",[34,1352,1353],{"class":36,"line":670},[34,1354,1355],{"class":40},"\u002F\u002F     \u003Cdiv>Loading map…\u003C\u002Fdiv>  \u003C!-- server + initial client -->\n",[34,1357,1358],{"class":36,"line":675},[34,1359,1360],{"class":40},"\u002F\u002F   \u003C\u002Ftemplate>\n",[34,1362,1363],{"class":36,"line":681},[34,1364,1365],{"class":40},"\u002F\u002F \u003C\u002FClientOnly>\n",[34,1367,1368],{"class":36,"line":693},[34,1369,84],{"emptyLinePlaceholder":83},[34,1371,1372],{"class":36,"line":706},[34,1373,1374],{"class":40},"\u002F\u002F ── 5. SSR-safe window access: import.meta.client ──\n",[34,1376,1377,1380,1382,1384,1386,1389],{"class":36,"line":711},[34,1378,1379],{"class":172},"if",[34,1381,340],{"class":176},[34,1383,173],{"class":172},[34,1385,606],{"class":176},[34,1387,1388],{"class":196},"meta",[34,1390,1391],{"class":176},".client) {\n",[34,1393,1394],{"class":36,"line":717},[34,1395,1396],{"class":40},"  \u002F\u002F Client-only code — safely skipped on server\n",[34,1398,1399,1402,1405,1407,1410],{"class":36,"line":725},[34,1400,1401],{"class":176},"  window.",[34,1403,1404],{"class":203},"addEventListener",[34,1406,207],{"class":176},[34,1408,1409],{"class":183},"'resize'",[34,1411,1412],{"class":176},", handler)\n",[34,1414,1415],{"class":36,"line":739},[34,1416,667],{"class":176},[15,1418,1420],{"id":1419},"ssr-safe-composables","SSR-Safe Composables",[20,1422,1424],{"filename":1423,"language":23},"ssr-safe-composables.ts",[25,1425,1427],{"className":27,"code":1426,"language":23,"meta":29,"style":29},"import { ref, onMounted, onScopeDispose, watch, type Ref } from 'vue'\n\n\u002F\u002F ── SSR-safe composable: handles server (no DOM) and client ──\nexport function useWindowWidth(): Ref\u003Cnumber> {\n  const width = ref(0)\n\n  \u002F\u002F ── onMounted: client-only — server never runs it ──\n  onMounted(() => {\n    width.value = window.innerWidth\n    const handler = () => { width.value = window.innerWidth }\n    window.addEventListener('resize', handler)\n    onScopeDispose(() => window.removeEventListener('resize', handler))\n  })\n\n  \u002F\u002F ── Server: width stays 0 (no window) ──\n  \u002F\u002F Client: width updates on mount (hydration-compatible — 0 matches server)\n  return width\n}\n\n\u002F\u002F ── SSR-safe localStorage composable ──\nexport function useLocalStorage(key: string, initial: string): Ref\u003Cstring> {\n  const value = ref(initial)\n\n  \u002F\u002F ── onMounted: client-only — localStorage doesn't exist on server ──\n  onMounted(() => {\n    const stored = localStorage.getItem(key)\n    if (stored !== null) value.value = stored\n\n    \u002F\u002F Watch: save to localStorage on change\n    watch(value, (newVal) => {\n      localStorage.setItem(key, newVal)\n    })\n  })\n\n  return value\n}\n",[31,1428,1429,1446,1450,1455,1482,1498,1502,1507,1518,1528,1548,1561,1583,1588,1592,1597,1602,1609,1613,1617,1622,1664,1678,1682,1687,1697,1715,1737,1741,1746,1764,1775,1780,1784,1788,1795],{"__ignoreMap":29},[34,1430,1431,1433,1436,1439,1442,1444],{"class":36,"line":37},[34,1432,173],{"class":172},[34,1434,1435],{"class":176}," { ref, onMounted, onScopeDispose, watch, ",[34,1437,1438],{"class":172},"type",[34,1440,1441],{"class":176}," Ref } ",[34,1443,180],{"class":172},[34,1445,184],{"class":183},[34,1447,1448],{"class":36,"line":44},[34,1449,84],{"emptyLinePlaceholder":83},[34,1451,1452],{"class":36,"line":50},[34,1453,1454],{"class":40},"\u002F\u002F ── SSR-safe composable: handles server (no DOM) and client ──\n",[34,1456,1457,1459,1461,1464,1467,1470,1473,1476,1479],{"class":36,"line":56},[34,1458,464],{"class":172},[34,1460,470],{"class":172},[34,1462,1463],{"class":203}," useWindowWidth",[34,1465,1466],{"class":176},"()",[34,1468,1469],{"class":172},":",[34,1471,1472],{"class":203}," Ref",[34,1474,1475],{"class":176},"\u003C",[34,1477,1478],{"class":196},"number",[34,1480,1481],{"class":176},"> {\n",[34,1483,1484,1486,1488,1490,1492,1494,1496],{"class":36,"line":62},[34,1485,256],{"class":172},[34,1487,991],{"class":196},[34,1489,200],{"class":172},[34,1491,204],{"class":203},[34,1493,207],{"class":176},[34,1495,1000],{"class":196},[34,1497,213],{"class":176},[34,1499,1500],{"class":36,"line":68},[34,1501,84],{"emptyLinePlaceholder":83},[34,1503,1504],{"class":36,"line":74},[34,1505,1506],{"class":40},"  \u002F\u002F ── onMounted: client-only — server never runs it ──\n",[34,1508,1509,1512,1514,1516],{"class":36,"line":80},[34,1510,1511],{"class":203},"  onMounted",[34,1513,879],{"class":176},[34,1515,248],{"class":172},[34,1517,251],{"class":176},[34,1519,1520,1523,1525],{"class":36,"line":87},[34,1521,1522],{"class":176},"    width.value ",[34,1524,282],{"class":172},[34,1526,1527],{"class":176}," window.innerWidth\n",[34,1529,1530,1532,1535,1537,1539,1541,1543,1545],{"class":36,"line":93},[34,1531,351],{"class":172},[34,1533,1534],{"class":203}," handler",[34,1536,200],{"class":172},[34,1538,245],{"class":176},[34,1540,248],{"class":172},[34,1542,1028],{"class":176},[34,1544,282],{"class":172},[34,1546,1547],{"class":176}," window.innerWidth }\n",[34,1549,1550,1553,1555,1557,1559],{"class":36,"line":99},[34,1551,1552],{"class":176},"    window.",[34,1554,1404],{"class":203},[34,1556,207],{"class":176},[34,1558,1409],{"class":183},[34,1560,1412],{"class":176},[34,1562,1563,1566,1568,1570,1573,1576,1578,1580],{"class":36,"line":105},[34,1564,1565],{"class":203},"    onScopeDispose",[34,1567,879],{"class":176},[34,1569,248],{"class":172},[34,1571,1572],{"class":176}," window.",[34,1574,1575],{"class":203},"removeEventListener",[34,1577,207],{"class":176},[34,1579,1409],{"class":183},[34,1581,1582],{"class":176},", handler))\n",[34,1584,1585],{"class":36,"line":111},[34,1586,1587],{"class":176},"  })\n",[34,1589,1590],{"class":36,"line":117},[34,1591,84],{"emptyLinePlaceholder":83},[34,1593,1594],{"class":36,"line":123},[34,1595,1596],{"class":40},"  \u002F\u002F ── Server: width stays 0 (no window) ──\n",[34,1598,1599],{"class":36,"line":129},[34,1600,1601],{"class":40},"  \u002F\u002F Client: width updates on mount (hydration-compatible — 0 matches server)\n",[34,1603,1604,1606],{"class":36,"line":134},[34,1605,628],{"class":172},[34,1607,1608],{"class":176}," width\n",[34,1610,1611],{"class":36,"line":140},[34,1612,667],{"class":176},[34,1614,1615],{"class":36,"line":146},[34,1616,84],{"emptyLinePlaceholder":83},[34,1618,1619],{"class":36,"line":152},[34,1620,1621],{"class":40},"\u002F\u002F ── SSR-safe localStorage composable ──\n",[34,1623,1624,1626,1628,1631,1633,1636,1638,1641,1643,1646,1648,1650,1653,1655,1657,1659,1662],{"class":36,"line":389},[34,1625,464],{"class":172},[34,1627,470],{"class":172},[34,1629,1630],{"class":203}," useLocalStorage",[34,1632,207],{"class":176},[34,1634,1635],{"class":1171},"key",[34,1637,1469],{"class":172},[34,1639,1640],{"class":196}," string",[34,1642,1181],{"class":176},[34,1644,1645],{"class":1171},"initial",[34,1647,1469],{"class":172},[34,1649,1640],{"class":196},[34,1651,1652],{"class":176},")",[34,1654,1469],{"class":172},[34,1656,1472],{"class":203},[34,1658,1475],{"class":176},[34,1660,1661],{"class":196},"string",[34,1663,1481],{"class":176},[34,1665,1666,1668,1671,1673,1675],{"class":36,"line":619},[34,1667,256],{"class":172},[34,1669,1670],{"class":196}," value",[34,1672,200],{"class":172},[34,1674,204],{"class":203},[34,1676,1677],{"class":176},"(initial)\n",[34,1679,1680],{"class":36,"line":625},[34,1681,84],{"emptyLinePlaceholder":83},[34,1683,1684],{"class":36,"line":634},[34,1685,1686],{"class":40},"  \u002F\u002F ── onMounted: client-only — localStorage doesn't exist on server ──\n",[34,1688,1689,1691,1693,1695],{"class":36,"line":646},[34,1690,1511],{"class":203},[34,1692,879],{"class":176},[34,1694,248],{"class":172},[34,1696,251],{"class":176},[34,1698,1699,1701,1704,1706,1709,1712],{"class":36,"line":658},[34,1700,351],{"class":172},[34,1702,1703],{"class":196}," stored",[34,1705,200],{"class":172},[34,1707,1708],{"class":176}," localStorage.",[34,1710,1711],{"class":203},"getItem",[34,1713,1714],{"class":176},"(key)\n",[34,1716,1717,1720,1723,1726,1729,1732,1734],{"class":36,"line":664},[34,1718,1719],{"class":172},"    if",[34,1721,1722],{"class":176}," (stored ",[34,1724,1725],{"class":172},"!==",[34,1727,1728],{"class":196}," null",[34,1730,1731],{"class":176},") value.value ",[34,1733,282],{"class":172},[34,1735,1736],{"class":176}," stored\n",[34,1738,1739],{"class":36,"line":670},[34,1740,84],{"emptyLinePlaceholder":83},[34,1742,1743],{"class":36,"line":675},[34,1744,1745],{"class":40},"    \u002F\u002F Watch: save to localStorage on change\n",[34,1747,1748,1751,1754,1757,1760,1762],{"class":36,"line":681},[34,1749,1750],{"class":203},"    watch",[34,1752,1753],{"class":176},"(value, (",[34,1755,1756],{"class":1171},"newVal",[34,1758,1759],{"class":176},") ",[34,1761,248],{"class":172},[34,1763,251],{"class":176},[34,1765,1766,1769,1772],{"class":36,"line":693},[34,1767,1768],{"class":176},"      localStorage.",[34,1770,1771],{"class":203},"setItem",[34,1773,1774],{"class":176},"(key, newVal)\n",[34,1776,1777],{"class":36,"line":706},[34,1778,1779],{"class":176},"    })\n",[34,1781,1782],{"class":36,"line":711},[34,1783,1587],{"class":176},[34,1785,1786],{"class":36,"line":717},[34,1787,84],{"emptyLinePlaceholder":83},[34,1789,1790,1792],{"class":36,"line":725},[34,1791,628],{"class":172},[34,1793,1794],{"class":176}," value\n",[34,1796,1797],{"class":36,"line":739},[34,1798,667],{"class":176},[15,1800,1802],{"id":1801},"tips-tricks","💡 Tips & Tricks",[20,1804,1806],{"filename":1805,"language":23},"tips.ts",[25,1807,1809],{"className":27,"code":1808,"language":23,"meta":29,"style":29},"\u002F\u002F ── 1. createSSRApp vs createApp ──\n\u002F\u002F createSSRApp: hydrates existing DOM (SSR) — doesn't re-render on mount.\n\u002F\u002F createApp: replaces DOM (SPA) — creates new DOM from scratch.\n\u002F\u002F Using createApp for SSR: full re-render, hydration warning, flash of content.\n\n\u002F\u002F ── 2. Hydration: Vue assumes server HTML is correct ──\n\u002F\u002F If mismatch: Vue discards server HTML, re-renders. Visible flash + perf cost.\n\u002F\u002F Debug: run in dev mode — Vue logs hydration mismatch warnings with details.\n\n\u002F\u002F ── 3. Pinia: createPinia per request on server ──\n\u002F\u002F Module-scoped Pinia leaks state between requests (security bug).\n\u002F\u002F Nuxt handles this automatically; custom SSR must create per-request.\n\n\u002F\u002F ── 4. ClientOnly: skip SSR for client-only components ──\n\u002F\u002F Maps, charts, webcams — anything requiring window\u002Fcanvas.\n\u002F\u002F Server renders fallback, client renders real content after hydration.\n\n\u002F\u002F ── 5. import.meta.client \u002F import.meta.server (Nuxt\u002FVite) ──\n\u002F\u002F Tree-shakeable: client-only code removed from server bundle and vice versa.\n\u002F\u002F Better than typeof window checks (runtime check, not build-time elimination).\n",[31,1810,1811,1816,1821,1826,1831,1835,1840,1845,1850,1854,1859,1864,1869,1873,1878,1883,1888,1892,1897,1902],{"__ignoreMap":29},[34,1812,1813],{"class":36,"line":37},[34,1814,1815],{"class":40},"\u002F\u002F ── 1. createSSRApp vs createApp ──\n",[34,1817,1818],{"class":36,"line":44},[34,1819,1820],{"class":40},"\u002F\u002F createSSRApp: hydrates existing DOM (SSR) — doesn't re-render on mount.\n",[34,1822,1823],{"class":36,"line":50},[34,1824,1825],{"class":40},"\u002F\u002F createApp: replaces DOM (SPA) — creates new DOM from scratch.\n",[34,1827,1828],{"class":36,"line":56},[34,1829,1830],{"class":40},"\u002F\u002F Using createApp for SSR: full re-render, hydration warning, flash of content.\n",[34,1832,1833],{"class":36,"line":62},[34,1834,84],{"emptyLinePlaceholder":83},[34,1836,1837],{"class":36,"line":68},[34,1838,1839],{"class":40},"\u002F\u002F ── 2. Hydration: Vue assumes server HTML is correct ──\n",[34,1841,1842],{"class":36,"line":74},[34,1843,1844],{"class":40},"\u002F\u002F If mismatch: Vue discards server HTML, re-renders. Visible flash + perf cost.\n",[34,1846,1847],{"class":36,"line":80},[34,1848,1849],{"class":40},"\u002F\u002F Debug: run in dev mode — Vue logs hydration mismatch warnings with details.\n",[34,1851,1852],{"class":36,"line":87},[34,1853,84],{"emptyLinePlaceholder":83},[34,1855,1856],{"class":36,"line":93},[34,1857,1858],{"class":40},"\u002F\u002F ── 3. Pinia: createPinia per request on server ──\n",[34,1860,1861],{"class":36,"line":99},[34,1862,1863],{"class":40},"\u002F\u002F Module-scoped Pinia leaks state between requests (security bug).\n",[34,1865,1866],{"class":36,"line":105},[34,1867,1868],{"class":40},"\u002F\u002F Nuxt handles this automatically; custom SSR must create per-request.\n",[34,1870,1871],{"class":36,"line":111},[34,1872,84],{"emptyLinePlaceholder":83},[34,1874,1875],{"class":36,"line":117},[34,1876,1877],{"class":40},"\u002F\u002F ── 4. ClientOnly: skip SSR for client-only components ──\n",[34,1879,1880],{"class":36,"line":123},[34,1881,1882],{"class":40},"\u002F\u002F Maps, charts, webcams — anything requiring window\u002Fcanvas.\n",[34,1884,1885],{"class":36,"line":129},[34,1886,1887],{"class":40},"\u002F\u002F Server renders fallback, client renders real content after hydration.\n",[34,1889,1890],{"class":36,"line":134},[34,1891,84],{"emptyLinePlaceholder":83},[34,1893,1894],{"class":36,"line":140},[34,1895,1896],{"class":40},"\u002F\u002F ── 5. import.meta.client \u002F import.meta.server (Nuxt\u002FVite) ──\n",[34,1898,1899],{"class":36,"line":146},[34,1900,1901],{"class":40},"\u002F\u002F Tree-shakeable: client-only code removed from server bundle and vice versa.\n",[34,1903,1904],{"class":36,"line":152},[34,1905,1906],{"class":40},"\u002F\u002F Better than typeof window checks (runtime check, not build-time elimination).\n",[15,1908,1910],{"id":1909},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[20,1912,1914],{"filename":1913,"language":23},"edge-cases.ts",[25,1915,1917],{"className":27,"code":1916,"language":23,"meta":29,"style":29},"\u002F\u002F ── 1. Module-scoped state leaks across SSR requests ──\n\u002F\u002F const shared = ref(0) at module scope → Request A's data visible to Request B.\n\u002F\u002F Fix: create state per-request (Pinia, or factory function in app creation).\n\n\u002F\u002F ── 2. onServerPrefetch errors crash SSR if not caught ──\n\u002F\u002F If onServerPrefetch throws, the entire server render fails (500 error).\n\u002F\u002F Wrap in try\u002Fcatch, or use Nuxt's useFetch (auto error handling).\n\n\u002F\u002F ── 3. Hydration mismatch on Date formatting ──\n\u002F\u002F Server: UTC timezone. Client: user's timezone.\n\u002F\u002F Same timestamp → different formatted strings → mismatch.\n\u002F\u002F Fix: format on client only (ClientOnly) or use UTC consistently.\n\n\u002F\u002F ── 4. Teleport to=\"body\" breaks on server (no document) ──\n\u002F\u002F Use :disabled=\"typeof document === 'undefined'\" or Nuxt's \u003CTeleport> wrapper.\n\u002F\u002F Server renders in-place, client teleports on hydration.\n\n\u002F\u002F ── 5. Third-party libraries requiring window crash SSR ──\n\u002F\u002F Wrap in \u003CClientOnly> or import dynamically in onMounted.\n\u002F\u002F Or: use Nuxt plugin with mode: 'client' (only registered on client).\n\n\u002F\u002F ── 6. Async setup + SSR: requires Suspense ──\n\u002F\u002F Top-level await in setup makes the component async.\n\u002F\u002F On server: renderToString waits for it. On client: needs \u003CSuspense>.\n",[31,1918,1919,1924,1929,1934,1938,1943,1948,1953,1957,1962,1967,1972,1977,1981,1986,1991,1996,2000,2005,2010,2015,2019,2024,2029],{"__ignoreMap":29},[34,1920,1921],{"class":36,"line":37},[34,1922,1923],{"class":40},"\u002F\u002F ── 1. Module-scoped state leaks across SSR requests ──\n",[34,1925,1926],{"class":36,"line":44},[34,1927,1928],{"class":40},"\u002F\u002F const shared = ref(0) at module scope → Request A's data visible to Request B.\n",[34,1930,1931],{"class":36,"line":50},[34,1932,1933],{"class":40},"\u002F\u002F Fix: create state per-request (Pinia, or factory function in app creation).\n",[34,1935,1936],{"class":36,"line":56},[34,1937,84],{"emptyLinePlaceholder":83},[34,1939,1940],{"class":36,"line":62},[34,1941,1942],{"class":40},"\u002F\u002F ── 2. onServerPrefetch errors crash SSR if not caught ──\n",[34,1944,1945],{"class":36,"line":68},[34,1946,1947],{"class":40},"\u002F\u002F If onServerPrefetch throws, the entire server render fails (500 error).\n",[34,1949,1950],{"class":36,"line":74},[34,1951,1952],{"class":40},"\u002F\u002F Wrap in try\u002Fcatch, or use Nuxt's useFetch (auto error handling).\n",[34,1954,1955],{"class":36,"line":80},[34,1956,84],{"emptyLinePlaceholder":83},[34,1958,1959],{"class":36,"line":87},[34,1960,1961],{"class":40},"\u002F\u002F ── 3. Hydration mismatch on Date formatting ──\n",[34,1963,1964],{"class":36,"line":93},[34,1965,1966],{"class":40},"\u002F\u002F Server: UTC timezone. Client: user's timezone.\n",[34,1968,1969],{"class":36,"line":99},[34,1970,1971],{"class":40},"\u002F\u002F Same timestamp → different formatted strings → mismatch.\n",[34,1973,1974],{"class":36,"line":105},[34,1975,1976],{"class":40},"\u002F\u002F Fix: format on client only (ClientOnly) or use UTC consistently.\n",[34,1978,1979],{"class":36,"line":111},[34,1980,84],{"emptyLinePlaceholder":83},[34,1982,1983],{"class":36,"line":117},[34,1984,1985],{"class":40},"\u002F\u002F ── 4. Teleport to=\"body\" breaks on server (no document) ──\n",[34,1987,1988],{"class":36,"line":123},[34,1989,1990],{"class":40},"\u002F\u002F Use :disabled=\"typeof document === 'undefined'\" or Nuxt's \u003CTeleport> wrapper.\n",[34,1992,1993],{"class":36,"line":129},[34,1994,1995],{"class":40},"\u002F\u002F Server renders in-place, client teleports on hydration.\n",[34,1997,1998],{"class":36,"line":134},[34,1999,84],{"emptyLinePlaceholder":83},[34,2001,2002],{"class":36,"line":140},[34,2003,2004],{"class":40},"\u002F\u002F ── 5. Third-party libraries requiring window crash SSR ──\n",[34,2006,2007],{"class":36,"line":146},[34,2008,2009],{"class":40},"\u002F\u002F Wrap in \u003CClientOnly> or import dynamically in onMounted.\n",[34,2011,2012],{"class":36,"line":152},[34,2013,2014],{"class":40},"\u002F\u002F Or: use Nuxt plugin with mode: 'client' (only registered on client).\n",[34,2016,2017],{"class":36,"line":389},[34,2018,84],{"emptyLinePlaceholder":83},[34,2020,2021],{"class":36,"line":619},[34,2022,2023],{"class":40},"\u002F\u002F ── 6. Async setup + SSR: requires Suspense ──\n",[34,2025,2026],{"class":36,"line":625},[34,2027,2028],{"class":40},"\u002F\u002F Top-level await in setup makes the component async.\n",[34,2030,2031],{"class":36,"line":634},[34,2032,2033],{"class":40},"\u002F\u002F On server: renderToString waits for it. On client: needs \u003CSuspense>.\n",[15,2035,2037],{"id":2036},"spot-the-bug","🧠 Spot the Bug",[2039,2040,2041],"p",{},"An SSR app shows a hydration mismatch warning for a clock component.",[20,2043,2046],{"filename":2044,"language":2045},"ClockBug.vue","vue",[25,2047,2050],{"className":2048,"code":2049,"language":2045,"meta":29,"style":29},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup>\nimport { ref, onMounted } from 'vue'\n\nconst time = ref(new Date().toLocaleTimeString())  \u002F\u002F ← computed at setup time\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>Current time: {{ time }}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[31,2051,2052,2066,2077,2081,2108,2117,2121,2130,2145],{"__ignoreMap":29},[34,2053,2054,2056,2060,2063],{"class":36,"line":37},[34,2055,1475],{"class":176},[34,2057,2059],{"class":2058},"sk71V","script",[34,2061,2062],{"class":203}," setup",[34,2064,2065],{"class":176},">\n",[34,2067,2068,2070,2073,2075],{"class":36,"line":44},[34,2069,173],{"class":172},[34,2071,2072],{"class":176}," { ref, onMounted } ",[34,2074,180],{"class":172},[34,2076,184],{"class":183},[34,2078,2079],{"class":36,"line":50},[34,2080,84],{"emptyLinePlaceholder":83},[34,2082,2083,2085,2088,2090,2092,2094,2096,2098,2100,2102,2105],{"class":36,"line":56},[34,2084,193],{"class":172},[34,2086,2087],{"class":196}," time",[34,2089,200],{"class":172},[34,2091,204],{"class":203},[34,2093,207],{"class":176},[34,2095,844],{"class":172},[34,2097,847],{"class":203},[34,2099,900],{"class":176},[34,2101,903],{"class":203},[34,2103,2104],{"class":176},"())  ",[34,2106,2107],{"class":40},"\u002F\u002F ← computed at setup time\n",[34,2109,2110,2113,2115],{"class":36,"line":62},[34,2111,2112],{"class":176},"\u003C\u002F",[34,2114,2059],{"class":2058},[34,2116,2065],{"class":176},[34,2118,2119],{"class":36,"line":68},[34,2120,84],{"emptyLinePlaceholder":83},[34,2122,2123,2125,2128],{"class":36,"line":74},[34,2124,1475],{"class":176},[34,2126,2127],{"class":2058},"template",[34,2129,2065],{"class":176},[34,2131,2132,2135,2138,2141,2143],{"class":36,"line":80},[34,2133,2134],{"class":176},"  \u003C",[34,2136,2137],{"class":2058},"div",[34,2139,2140],{"class":176},">Current time: {{ time }}\u003C\u002F",[34,2142,2137],{"class":2058},[34,2144,2065],{"class":176},[34,2146,2147,2149,2151],{"class":36,"line":87},[34,2148,2112],{"class":176},[34,2150,2127],{"class":2058},[34,2152,2065],{"class":176},[2154,2155,2156,2160,2178,2185,2315],"details",{},[2157,2158,2159],"summary",{},"Answer",[2039,2161,2162,2165,2166,2169,2170,2172,2173,2177],{},[31,2163,2164],{},"new Date().toLocaleTimeString()"," runs during ",[31,2167,2168],{},"setup()"," — on the server, it captures the server's time at render time. On the client, ",[31,2171,2168],{}," runs during hydration and captures a ",[2174,2175,2176],"em",{},"different"," time (milliseconds or seconds later, possibly a different timezone). The server HTML says \"14:00:00.000\" but the client computes \"14:00:00.250\" — mismatch.",[2039,2179,2180,2184],{},[2181,2182,2183],"strong",{},"Fix"," — render the time only on the client (after hydration) or use a fixed timestamp from the server:",[20,2186,2188],{"filename":2187,"language":2045},"ClockFixed.vue",[25,2189,2191],{"className":2048,"code":2190,"language":2045,"meta":29,"style":29},"\u003Cscript setup>\nimport { ref, onMounted } from 'vue'\n\nconst time = ref('')\n\nonMounted(() => {\n  \u002F\u002F Client-only: updates after hydration — no mismatch\n  time.value = new Date().toLocaleTimeString()\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>Current time: {{ time || 'Loading…' }}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[31,2192,2193,2203,2213,2217,2234,2238,2248,2253,2270,2274,2282,2286,2294,2307],{"__ignoreMap":29},[34,2194,2195,2197,2199,2201],{"class":36,"line":37},[34,2196,1475],{"class":176},[34,2198,2059],{"class":2058},[34,2200,2062],{"class":203},[34,2202,2065],{"class":176},[34,2204,2205,2207,2209,2211],{"class":36,"line":44},[34,2206,173],{"class":172},[34,2208,2072],{"class":176},[34,2210,180],{"class":172},[34,2212,184],{"class":183},[34,2214,2215],{"class":36,"line":50},[34,2216,84],{"emptyLinePlaceholder":83},[34,2218,2219,2221,2223,2225,2227,2229,2232],{"class":36,"line":56},[34,2220,193],{"class":172},[34,2222,2087],{"class":196},[34,2224,200],{"class":172},[34,2226,204],{"class":203},[34,2228,207],{"class":176},[34,2230,2231],{"class":183},"''",[34,2233,213],{"class":176},[34,2235,2236],{"class":36,"line":62},[34,2237,84],{"emptyLinePlaceholder":83},[34,2239,2240,2242,2244,2246],{"class":36,"line":68},[34,2241,322],{"class":203},[34,2243,879],{"class":176},[34,2245,248],{"class":172},[34,2247,251],{"class":176},[34,2249,2250],{"class":36,"line":74},[34,2251,2252],{"class":40},"  \u002F\u002F Client-only: updates after hydration — no mismatch\n",[34,2254,2255,2258,2260,2262,2264,2266,2268],{"class":36,"line":80},[34,2256,2257],{"class":176},"  time.value ",[34,2259,282],{"class":172},[34,2261,895],{"class":172},[34,2263,847],{"class":203},[34,2265,900],{"class":176},[34,2267,903],{"class":203},[34,2269,293],{"class":176},[34,2271,2272],{"class":36,"line":87},[34,2273,298],{"class":176},[34,2275,2276,2278,2280],{"class":36,"line":93},[34,2277,2112],{"class":176},[34,2279,2059],{"class":2058},[34,2281,2065],{"class":176},[34,2283,2284],{"class":36,"line":99},[34,2285,84],{"emptyLinePlaceholder":83},[34,2287,2288,2290,2292],{"class":36,"line":105},[34,2289,1475],{"class":176},[34,2291,2127],{"class":2058},[34,2293,2065],{"class":176},[34,2295,2296,2298,2300,2303,2305],{"class":36,"line":111},[34,2297,2134],{"class":176},[34,2299,2137],{"class":2058},[34,2301,2302],{"class":176},">Current time: {{ time || 'Loading…' }}\u003C\u002F",[34,2304,2137],{"class":2058},[34,2306,2065],{"class":176},[34,2308,2309,2311,2313],{"class":36,"line":117},[34,2310,2112],{"class":176},[34,2312,2127],{"class":2058},[34,2314,2065],{"class":176},[2039,2316,2317,2320,2321,2323],{},[2181,2318,2319],{},"The lesson",": any value that depends on \"when\" it's computed (Date, Math.random, performance.now) will differ between server render and client hydration. Compute it in ",[31,2322,322],{}," (client-only) or use a fixed value from server-side data.",[2325,2326,2327],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}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 .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .sk71V, html code.shiki .sk71V{--shiki-default:#22863A;--shiki-github-dark:#85E89D}",{"title":29,"searchDepth":44,"depth":44,"links":2329},[2330,2331,2332,2333,2334,2335,2336,2337,2338],{"id":17,"depth":44,"text":18},{"id":158,"depth":44,"text":159},{"id":394,"depth":44,"text":395},{"id":799,"depth":44,"text":800},{"id":1114,"depth":44,"text":1115},{"id":1419,"depth":44,"text":1420},{"id":1801,"depth":44,"text":1802},{"id":1909,"depth":44,"text":1910},{"id":2036,"depth":44,"text":2037},"Server-side rendering hydration, onServerPrefetch data loading, state serialization, hydration mismatch causes, Nuxt universal rendering, and SSR-safe composables.","md",{},"\u002Fvue\u002F20-ssr-and-nuxt-basics",{"title":5,"description":2339},"vue\u002F20-ssr-and-nuxt-basics","43zvYDC0x_PJHmmS4_kSKQxfifB3Jymp-8u3El9KRsM",1789924655836]