{"id":27,"date":"2026-06-06T21:49:02","date_gmt":"2026-06-06T21:49:02","guid":{"rendered":"https:\/\/softwareproduction.eu\/wordpress\/?p=27"},"modified":"2026-06-06T23:44:53","modified_gmt":"2026-06-06T23:44:53","slug":"ssr-deep-dive-hydration-state-replay-and-the-cookbook","status":"publish","type":"post","link":"https:\/\/softwareproduction.eu\/?p=27","title":{"rendered":"SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Twelfth in a series about migrating from legacy architectures to a modern Nuxt 4 stack.<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Hydration Contract<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In a server-rendered Vue application, SSR establishes a strict contract: the HTML generated on the server must match <strong>exactly<\/strong> what the client-side Vue runtime would render. During hydration, Vue attaches to the existing DOM instead of re-rendering it from scratch. If the server HTML and the client render differ, Vue reports a <strong>hydration mismatch<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In Vue 3 strict mode (and Nuxt 4), hydration mismatches are more than harmless warnings. They can lead to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Silent rendering bugs (server HTML stays, but event listeners bind to the wrong elements)<\/li>\n<li>Missing interactivity (Vue skips hydrating mismatched subtrees)<\/li>\n<li>Inconsistent state (server-rendered content shows one value, client state holds another)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">These issues are tricky because they only appear under SSR \u2014 the same component may work perfectly in client-only mode.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">A Taxonomy of Hydration Mismatches<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Across large enterprise applications, most hydration issues fall into a handful of categories. Once you recognize the category, the fix usually becomes obvious.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Category 1: Non-Deterministic Values<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Any value that differs between server and client at render time will cause a mismatch:<\/p>\n\n\n\n<pre><code class=\"language-text\">Server renders:  &lt;div id=\"input-a7f3b2\"&gt;...&lt;\/div&gt;\nClient renders:  &lt;div id=\"input-c9e1d4\"&gt;...&lt;\/div&gt;\n                              \u2191 different random value<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Common culprits<\/strong>: <code>Math.random()<\/code>, <code>Date.now()<\/code>, <code>crypto.randomUUID()<\/code> used in templates or <code>setup()<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix<\/strong>: use <code>useId()<\/code> \u2014 a Nuxt composable that generates deterministic IDs, consistent between server and client.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Category 2: Timing-Dependent State<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If a child component mutates parent state during <code>setup()<\/code>, the execution order can differ between server and client:<\/p>\n\n\n\n<pre class=\"mermaid\">sequenceDiagram\n    box Server\n      participant SParent as Parent (server)\n      participant SChild as Child (server)\n    end\n    box Client\n      participant CParent as Parent (client)\n      participant CChild as Child (client)\n    end\n\n    Note over SParent: 1. Parent setup()\n    SParent-&gt;&gt;SParent: setup()\n    Note over SParent: 2. Parent renders\n    SParent-&gt;&gt;SParent: render()\n    Note over SChild: 3. Child setup()&lt;br\/&gt;\u2192 emits to parent (too late for render)\n    SChild-&gt;&gt;SParent: emit() changes parent state\n\n    Note over CParent: 1. Parent setup()\n    CParent-&gt;&gt;CParent: setup()\n    Note over CChild: 2. Child setup()&lt;br\/&gt;\u2192 emits to parent\n    CChild-&gt;&gt;CParent: emit() changes parent state\n    Note over CParent: 3. Parent renders&lt;br\/&gt;with new state\n    CParent-&gt;&gt;CParent: render()\n\n    Note over SParent,CParent: Different HTML on server vs client<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix<\/strong>: move shared state into <code>useState()<\/code> so it is initialized once, independent of component execution order.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Category 3: Teleports<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code><Teleport to=\"body\"><\/code> is rendered inline in the component tree on the server, but moved to <code><body><\/code> on the client. The DOM structure no longer matches.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix<\/strong>: wrap teleported content in <code><ClientOnly><\/code> so it is rendered exclusively on the client.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n    subgraph SSRTree[\"SSR Tree\"]\n      A[\"Component A&lt;br\/&gt;(includes Teleport target)\"]\n      B[\"Teleported content&lt;br\/&gt;(rendered inline on server)\"]\n      A --&gt; B\n    end\n\n    subgraph HydratedDOM[\"Hydrated DOM\"]\n      A2[\"Component A&lt;br\/&gt;(no inline teleported content)\"]\n      B2[\"Teleported content&lt;br\/&gt;moved under body element\"]\n    end\n\n    SSRTree --&gt;|server HTML| HydratedDOM\n    classDef mismatch fill:#ffe0e0,stroke:#ff5555,stroke-width:1px;\n    class B,B2 mismatch;<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Category 4: Client-Side State Initialization<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If a reactive value is <code>false<\/code> during SSR but becomes <code>true<\/code> during hydration (for example, a dialog\u2019s <code>isOpen<\/code> toggled in <code>mounted<\/code>), CSS classes and markup diverge:<\/p>\n\n\n\n<pre><code class=\"language-text\">Server: &lt;div class=\"panel panel-closed\"&gt;   \u2190 isOpen = false\nClient: &lt;div class=\"panel panel-open\"&gt;     \u2190 isOpen = true (mounted set it)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix<\/strong>: ensure the initial value matches the SSR state. Use <code>watch<\/code> or <code>nextTick<\/code> to change state <em>after<\/em> hydration completes, not during.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Category 5: Async Composable Race Conditions<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When multiple composables use <code>useAsyncData<\/code> and depend on each other, the resolution order can differ between server and client. Computed values built on these async results may pass through different intermediate states and yield divergent HTML.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix<\/strong>: enforce top-down data flow from <code>useState<\/code>. Avoid computed values that depend on partially resolved async state.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    subgraph Server\n      S1[\"useAsyncData A&lt;br\/&gt;resolves first\"]\n      S2[\"useAsyncData B&lt;br\/&gt;resolves second\"]\n      SC[\"Computed C&lt;br\/&gt;based on A+B&lt;br\/&gt;\u2192 Server HTML\"]\n      S1 --&gt; SC\n      S2 --&gt; SC\n    end\n\n    subgraph Client\n      C1[\"useAsyncData B&lt;br\/&gt;resolves first\"]\n      C2[\"useAsyncData A&lt;br\/&gt;resolves second\"]\n      CC1[\"Computed C (intermediate)&lt;br\/&gt;based only on B\"]\n      CC2[\"Computed C (final)&lt;br\/&gt;based on A+B&lt;br\/&gt;\u2192 Client DOM\"]\n      C1 --&gt; CC1\n      C2 --&gt; CC2\n    end\n\n    classDef warn fill:#fff4e5,stroke:#ff9900,stroke-width:1px;\n    class SC,CC1,CC2 warn;<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Hydration Cookbook Pattern<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Capturing hydration issues in a structured way \u2014 symptom, root cause, fix \u2014 builds a shared knowledge base that dramatically reduces debugging time in any sizeable Nuxt application. A practical approach is to keep a \u201cHydration Issues Cookbook\u201d with entries like:<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    Issue[\"HYDRATION ISSUE:&lt;br\/&gt;Random IDs in Templates\"]\n\n    Symptom[\"Symptom:&lt;br\/&gt;#quot;Hydration node mismatch#quot;&lt;br\/&gt;&amp;lt;input id=#quot;...#quot;&amp;gt; differs\"]\n    Cause[\"Root Cause:&lt;br\/&gt;Math.random() \/ crypto.randomUUID()&lt;br\/&gt;in setup() or template\"]\n    Fix[\"Fix:&lt;br\/&gt;Use useId() for deterministic IDs\"]\n    Prevention[\"Prevention:&lt;br\/&gt;ESLint rule \u2014 no Math.random()&lt;br\/&gt;in setup\/template\"]\n\n    Issue --&gt; Symptom\n    Issue --&gt; Cause\n    Issue --&gt; Fix\n    Issue --&gt; Prevention\n\n    classDef header fill:#e0f2ff,stroke:#1e88e5,stroke-width:1px;\n    classDef box fill:#ffffff,stroke:#90a4ae,stroke-width:1px;\n    class Issue header;\n    class Symptom,Cause,Fix,Prevention box;<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each entry describes a <strong>pattern<\/strong>, not a one-off incident. Over time, teams learn to recognize categories instead of chasing isolated bugs.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">SSR Event Replay<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In large modular applications, events emitted during SSR still need to reach client-side listeners. The usual SSR lifecycle creates a timing gap:<\/p>\n\n\n\n<pre class=\"mermaid\">sequenceDiagram\n    box Server\n      participant S as Cart module (server)\n    end\n    box Client\n      participant C as Funnel module (client)\n    end\n\n    Note over S: Cart module loads\n    S-&gt;&gt;S: emit cart:loaded\n\n    Note over C: Hydration begins\n    C-&gt;&gt;C: subscribe to cart:loaded\n\n    Note over S,C: Event is lost \u2014&lt;br\/&gt;no client listener existed&lt;br\/&gt;when server emitted it<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The server fires events while rendering, but no client listeners exist yet. By the time they subscribe, those events are gone.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Solution: <code>useState<\/code> as an Event Buffer<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">During SSR, events are serialized into <code>useState<\/code>, which is automatically transferred from server to client via the Nuxt payload. After hydration, the event bus reads the stored events and replays them through standard RxJS subjects.<\/p>\n\n\n\n<pre class=\"mermaid\">sequenceDiagram\n    box Server\n      participant S as Cart module (server)\n      participant ST as useState (SSR store)\n    end\n    box Client\n      participant CT as useState (hydrated payload)\n      participant B as Event bus (RxJS)\n      participant L as Listeners\n    end\n\n    Note over S: Cart module loads\n    S-&gt;&gt;S: emit cart:loaded\n    S-&gt;&gt;ST: push cart:loaded into useState buffer\n\n    ST--&gt;&gt;CT: state transfer with events\n\n    Note over B,L: After hydration\n    L-&gt;&gt;B: subscribe to cart:loaded\n    B-&gt;&gt;CT: read buffered events\n    CT--&gt;&gt;B: cart:loaded events\n    B--&gt;&gt;L: replay cart:loaded&lt;br\/&gt;\u2192 listeners fire \u2713<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Replay is automatic. Module authors do not need to care whether an event fired during SSR or on the client \u2014 subscribers receive it either way.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Debugging Hydration Issues<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Hydration warnings identify <em>where<\/em> the DOM diverged, but rarely <em>why<\/em>. Vue points to a specific DOM node, while the real cause might be several layers up in the tree or hidden in composables.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Strategy 1: Binary Elimination<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Wrap parts of the page in <code><ClientOnly><\/code> to localize the mismatch. If wrapping section A in <code><ClientOnly><\/code> makes the warning disappear, the bug is in that section. Then progressively narrow down.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    Page[Page Component]\n\n    A[\"Section A&lt;br\/&gt;(suspect)\"]\n    B[Section B]\n    C[Section C]\n\n    Page --&gt; A\n    Page --&gt; B\n    Page --&gt; C\n\n    A2[\"Section A wrapped&lt;br\/&gt;in &amp;lt;ClientOnly&amp;gt;\"]\n    Page -. test step .-&gt; A2\n\n    classDef suspect fill:#fff4e5,stroke:#ff9800;\n    classDef normal fill:#ffffff,stroke:#90a4ae;\n    class A suspect;\n    class B,C normal;<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Strategy 2: SSR-Only Rendering<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Disable client-side hydration entirely (<code>ssr: true<\/code> with no client JavaScript) and compare:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The raw server HTML<\/li>\n<li>The HTML the client <em>would<\/em> render<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This isolates state differences and logic that only runs on the client.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n    SSR[\"SSR-only HTML&lt;br\/&gt;(no client JS)\"]\n    ClientRender[\"Client-only render&lt;br\/&gt;(same route, mocked data)\"]\n\n    SSR --&gt; Diff[Diff DOM + state]\n    ClientRender --&gt; Diff\n\n    Diff --&gt; Cause[\"Identify diverging values&lt;br\/&gt;and client-only logic\"]<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Strategy 3: AI-Assisted Debugging<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">An AI assistant connected to both the application\u2019s MCP server (for server-side state) and the browser\u2019s DevTools (for client-side state) can automatically diff the two:<\/p>\n\n\n\n<pre><code class=\"language-text\">Developer: \"The checkout form shows different content\n            after hydration. Help me debug this.\"\n\nAI Assistant:\n  1. Queries Pinia store via MCP \u2192 gets server-side cart state\n  2. Inspects browser DOM via DevTools \u2192 gets client-side rendering\n  3. Compares the two \u2192 identifies the diverging value\n  4. Traces the value to a composable with client-only initialization\n  5. Suggests fix: move initialization to useState<\/code><\/pre>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    Dev[Developer]\n    AI[AI Assistant]\n    MCP[\"MCP Server&lt;br\/&gt;(server-side state)\"]\n    DevTools[\"Browser DevTools&lt;br\/&gt;(client-side state)\"]\n    Diff[State + DOM diff]\n    Fix[\"Suggested fix&lt;br\/&gt;(e.g., move init to useState)\"]\n\n    Dev --&gt;|debug request| AI\n    AI --&gt; MCP\n    AI --&gt; DevTools\n    MCP --&gt; AI\n    DevTools --&gt; AI\n    AI --&gt; Diff\n    Diff --&gt; Fix\n    Fix --&gt; Dev<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This pattern is already used in practice in sophisticated internal tooling \u2014 for example, a debug-chatbot-style module that provides exactly this capability (covered in detail in Article 14 of this series).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The <code>hydrate-never<\/code> Directive<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Not all server-rendered content needs hydration. Static sections \u2014 text blocks, decorative images, layout wrappers \u2014 never change on the client. Hydrating them wastes CPU and inflates Total Blocking Time (TBT).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A custom directive marks elements that should be skipped during hydration:<\/p>\n\n\n\n<pre><code class=\"language-text\">With hydrate-never:\n  Server renders: &lt;div v-hydrate-never class=\"static-banner\"&gt;\n                    &lt;h2&gt;Welcome to Our Store&lt;\/h2&gt;\n                    &lt;p&gt;Thousands of satisfied customers...&lt;\/p&gt;\n                  &lt;\/div&gt;\n\n  Client: Vue skips this subtree during hydration\n          \u2192 No patch() calls\n          \u2192 No reactive tracking\n          \u2192 Zero TBT contribution<\/code><\/pre>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n    subgraph Render\n      S[\"Server render&lt;br\/&gt;&amp;lt;div v-hydrate-never&amp;gt;...\"]\n      C[\"Client hydration&lt;br\/&gt;Vue sees v-hydrate-never\"]\n    end\n\n    S --&gt;|HTML payload| C\n\n    C --&gt;|skip subtree| NoPatch[\"No patch() calls\"]\n    C --&gt;|skip reactivity| NoReactive[No reactive tracking]\n    C --&gt;|perf| TBT[\"Zero TBT contribution&lt;br\/&gt;for this subtree\"]\n\n    classDef static fill:#e0f7fa,stroke:#00acc1;\n    class S,C,NoPatch,NoReactive,TBT static;<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On pages with large static sections (landing pages, editorial content, catalog content), this can cut Total Blocking Time by 30\u201350%.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons Learned<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Hydration is a contract, not a feature<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Treating hydration as \u201cit works or it doesn\u2019t\u201d leads to brittle apps. Treating it as a <strong>contract<\/strong> \u2014 server and client must agree on every rendered value \u2014 leads to defensive patterns that prevent mismatches by design.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The five categories cover 95% of real-world mismatches<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Random values, timing-dependent state, teleports, client-side initialization, and async race conditions. If you know these five, you can diagnose almost any hydration issue you encounter.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Event replay is essential for SSR module architectures<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In modular SSR systems where modules communicate via events, event replay is non-negotiable. Without it, SSR-only events vanish on the client, creating subtle, production-only bugs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A cookbook is more valuable than documentation<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">High-level advice (\u201cavoid non-deterministic values\u201d) is less actionable than concrete patterns (\u201cthis code causes this bug; here is the fix\u201d). A living cookbook that evolves with new patterns is one of the most effective knowledge tools for hydration issues.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What\u2019s Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Article 10<\/strong>: <em>Memory, Stability, and PM2 \u2014 Running a Long-Lived Node.js Server<\/em> \u2014 What happens when V8 runs for days and how to keep it stable.<\/li>\n<li><strong>Article 11<\/strong>: <em>Multi-Environment Infrastructure \u2014 Azure Container Apps and the Configuration System<\/em> \u2014 Managing three environments with generated configuration.<\/li>\n<li><strong>Article 12<\/strong>: <em>Security in a Nuxt SSR App \u2014 CSRF, Azure AD, CSP, and More<\/em> \u2014 The security layers that protect a server-rendered application.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Twelfth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Hydration Contract In a server-rendered Vue application, SSR establishes a strict contract: the HTML generated on the server must match exactly what the client-side Vue runtime would render. During hydration, Vue attaches to the existing DOM instead of re-rendering [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":232,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[5],"tags":[],"class_list":["post-27","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-advanced-web-app-with-nuxt-and-net"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook - Scalable Web Production<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/softwareproduction.eu\/?p=27\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook - Scalable Web Production\" \/>\n<meta property=\"og:description\" content=\"Twelfth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Hydration Contract In a server-rendered Vue application, SSR establishes a strict contract: the HTML generated on the server must match exactly what the client-side Vue runtime would render. During hydration, Vue attaches to the existing DOM instead of re-rendering [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/softwareproduction.eu\/?p=27\" \/>\n<meta property=\"og:site_name\" content=\"Scalable Web Production\" \/>\n<meta property=\"article:published_time\" content=\"2026-06-06T21:49:02+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-06T23:44:53+00:00\" \/>\n<meta name=\"author\" content=\"Munir Husseini\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Munir Husseini\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"10 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=27#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=27\"},\"author\":{\"name\":\"Munir Husseini\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\"},\"headline\":\"SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook\",\"datePublished\":\"2026-06-06T21:49:02+00:00\",\"dateModified\":\"2026-06-06T23:44:53+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=27\"},\"wordCount\":1020,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=27#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/12-ssr-deep-dive.jpg\",\"articleSection\":[\"Advanced Web App with Nuxt and .NET\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=27#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=27\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=27\",\"name\":\"SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook - Scalable Web Production\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=27#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=27#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/12-ssr-deep-dive.jpg\",\"datePublished\":\"2026-06-06T21:49:02+00:00\",\"dateModified\":\"2026-06-06T23:44:53+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=27#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=27\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=27#primaryimage\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/12-ssr-deep-dive.jpg\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/12-ssr-deep-dive.jpg\",\"width\":1880,\"height\":1253},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=27#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/softwareproduction.eu\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"name\":\"Softwareproduction\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/softwareproduction.eu\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\",\"name\":\"Munir Husseini\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"width\":32,\"height\":32,\"caption\":\"Munir Husseini\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\",\"name\":\"Munir Husseini\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"caption\":\"Munir Husseini\"},\"sameAs\":[\"https:\\\/\\\/softwareproduction.eu\\\/\"],\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook - Scalable Web Production","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/softwareproduction.eu\/?p=27","og_locale":"en_US","og_type":"article","og_title":"SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook - Scalable Web Production","og_description":"Twelfth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Hydration Contract In a server-rendered Vue application, SSR establishes a strict contract: the HTML generated on the server must match exactly what the client-side Vue runtime would render. During hydration, Vue attaches to the existing DOM instead of re-rendering [&hellip;]","og_url":"https:\/\/softwareproduction.eu\/?p=27","og_site_name":"Scalable Web Production","article_published_time":"2026-06-06T21:49:02+00:00","article_modified_time":"2026-06-06T23:44:53+00:00","author":"Munir Husseini","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Munir Husseini","Est. reading time":"10 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/softwareproduction.eu\/?p=27#article","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/?p=27"},"author":{"name":"Munir Husseini","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f"},"headline":"SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook","datePublished":"2026-06-06T21:49:02+00:00","dateModified":"2026-06-06T23:44:53+00:00","mainEntityOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=27"},"wordCount":1020,"commentCount":0,"publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=27#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/12-ssr-deep-dive.jpg","articleSection":["Advanced Web App with Nuxt and .NET"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/softwareproduction.eu\/?p=27#respond"]}]},{"@type":"WebPage","@id":"https:\/\/softwareproduction.eu\/?p=27","url":"https:\/\/softwareproduction.eu\/?p=27","name":"SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook - Scalable Web Production","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/#website"},"primaryImageOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=27#primaryimage"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=27#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/12-ssr-deep-dive.jpg","datePublished":"2026-06-06T21:49:02+00:00","dateModified":"2026-06-06T23:44:53+00:00","breadcrumb":{"@id":"https:\/\/softwareproduction.eu\/?p=27#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/softwareproduction.eu\/?p=27"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/?p=27#primaryimage","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/12-ssr-deep-dive.jpg","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/12-ssr-deep-dive.jpg","width":1880,"height":1253},{"@type":"BreadcrumbList","@id":"https:\/\/softwareproduction.eu\/?p=27#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/softwareproduction.eu\/"},{"@type":"ListItem","position":2,"name":"SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook"}]},{"@type":"WebSite","@id":"https:\/\/softwareproduction.eu\/#website","url":"https:\/\/softwareproduction.eu\/","name":"Softwareproduction","description":"","publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/softwareproduction.eu\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/softwareproduction.eu\/#organization","name":"Munir Husseini","url":"https:\/\/softwareproduction.eu\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","width":32,"height":32,"caption":"Munir Husseini"},"image":{"@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f","name":"Munir Husseini","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","caption":"Munir Husseini"},"sameAs":["https:\/\/softwareproduction.eu\/"],"url":"https:\/\/softwareproduction.eu\/?author=1"}]}},"jetpack_featured_media_url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/12-ssr-deep-dive.jpg","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/27","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=27"}],"version-history":[{"count":9,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/27\/revisions"}],"predecessor-version":[{"id":233,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/27\/revisions\/233"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/media\/232"}],"wp:attachment":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=27"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=27"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=27"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}