Hydration Mismatches: When Your SPA Hides Content From Crawlers
A documentation site rebuilt on a modern React framework lost 60% of its indexed pages in five weeks, and the engineering team could not reproduce the problem in any browser. Every page loaded perfectly, content visible, canonical correct. The defect only appeared in the rendered DOM that Google’s renderer captured: during hydration, a layout component re-read the route and rewrote the canonical link to point at the section landing page instead of the article. Every article in a section hydrated to the same canonical, so Google consolidated dozens of distinct articles into one and dropped the rest. The server HTML was correct. The browser-visible page was correct. The post-hydration DOM — the only version Google indexes — was wrong.
This is the hydration mismatch problem, and it is insidious precisely because the symptom lives in a DOM state that humans rarely inspect. You look at the page in a browser and it is fine. You view-source and the server HTML is fine. The broken version exists for a few hundred milliseconds during hydration and then settles into a final DOM that may differ from both — and that final DOM is what gets indexed. The gap between what the server sends, what the framework expects, and what the page becomes after hydration is where canonicals, robots directives, titles, and entire content blocks quietly go wrong.
What Hydration Actually Does, and Where It Diverges
Server-side rendering produces HTML on the server and ships it to the browser as a complete document. The browser paints it immediately — fast first paint, content visible without waiting for JavaScript. Then the framework’s client bundle loads and “hydrates”: it walks the server-rendered DOM, attaches event listeners, and reconciles the static HTML with the component tree it would have rendered client-side, taking over the page to make it interactive.
Hydration assumes the server HTML and the client’s initial render match. When they do not, you have a mismatch, and frameworks handle it in ways that range from a console warning to a full client-side re-render that throws away the server HTML entirely. The divergence comes from a handful of recurring causes:
Non-deterministic rendering. Anything that produces different output on server and client breaks hydration. Date.now(), Math.random(), locale-dependent formatting, or reading window/localStorage (which exist only on the client) cause the client render to differ from the server HTML. The framework detects the mismatch and may re-render.
Conditional rendering on client-only state. Components that render one thing on the server (where there is no user, no viewport, no window.matchMedia) and another on the client cause a mismatch. A common pattern: if (isMobile) where isMobile is undefined on the server and true on the client, so the server sends desktop markup and the client swaps in mobile markup during hydration.
Effects that mutate critical tags. A useEffect or onMounted hook that updates the canonical, title, or meta robots after hydration changes the indexed signals. This is the documentation-site failure: the canonical was correct in server HTML, then a post-hydration effect rewrote it.
Suspense and streaming boundaries. Content inside a Suspense boundary that resolves client-side, or streamed-in chunks that arrive after the initial HTML, can leave content absent from the captured DOM if rendering completes before they resolve.
When hydration discards the server HTML and re-renders, the cost is not just a flicker. If the server-rendered content was the SEO-relevant content and the client re-render produces something different — or produces it later, after the renderer has captured the DOM — the indexed version is the wrong one.
Raw vs. Rendered: The Signals That Must Agree
Google’s indexing pipeline does not see one version of your page. It sees two, at two different times, and they must agree on the signals that matter.
First, Googlebot fetches the raw HTML — the server response, before any JavaScript runs. It reads this immediately: the raw canonical, the raw robots directive, the raw title, the links it can discover. Then the page is queued for rendering. Later — minutes to days later, depending on render budget — a headless Chromium instance loads the page, executes JavaScript, lets hydration complete and the network settle, and captures the rendered DOM. Google indexes that rendered DOM, but the raw HTML signals govern the interim and govern entirely if rendering fails or times out.
This two-phase model means specific signals must match across raw and rendered, or the page behaves unpredictably:
Canonical. If raw HTML declares canonical A and post-hydration JavaScript rewrites it to canonical B, Google sees both at different times and must reconcile them. At best it is confused; at worst it consolidates pages incorrectly. The canonical must be identical in raw and rendered.
Robots directive. This is the most dangerous mismatch. If raw HTML says index and hydration injects noindex (or vice versa), the page’s index eligibility becomes non-deterministic. Worse, a post-hydration noindex can deindex a page whose raw HTML and browser appearance both say it should be indexed. Robots directives changed by JavaScript are a known footgun precisely because the raw and rendered states disagree.
Title and meta description. Changed by hydration, these produce a mismatch between what Google initially reads and what it finally indexes, leading to unstable SERP snippets.
Primary content. If the meaningful content exists only in the rendered DOM — the raw HTML is an empty <div id="root"> and content is injected by JavaScript — then content discovery depends entirely on rendering succeeding. Any render failure, timeout, or blocked resource means Google indexes an empty shell. This is the failure mode that strands SPA content: the content is real, it renders in a browser, but it is absent from the raw HTML and the rendered capture missed it.
The robustness principle is that the raw HTML should already contain the SEO-critical signals and content in their final, correct state, and hydration should make the page interactive without changing any of those signals. Hydration that adds interactivity is fine. Hydration that changes the canonical, the robots directive, the title, or the primary content is a defect, because it creates a raw-versus-rendered divergence that the indexing pipeline resolves unpredictably.
Content That Only Appears After Interaction
Google’s renderer is not a user. It does not click, scroll deliberately, hover, or submit. It loads the page, executes JavaScript, waits for network activity to quiet down, and captures the DOM. Whatever requires an interaction to exist in the DOM does not get indexed.
This draws a sharp and often-misunderstood line:
Indexable — present but hidden. A tabbed interface where all tab panels are in the DOM and CSS hides the inactive ones. An accordion where every section’s text is in the HTML, collapsed with display:none or max-height. A “read more” that toggles visibility of text already in the DOM. In all of these the content is in the rendered DOM at load — it is merely visually hidden — so Google indexes it. Hidden-by-CSS is fine; the renderer reads the DOM, not the pixels.
Not indexable — fetched on interaction. A tab whose content is fetched from an API only when you click it. An accordion that lazy-loads its body on expand. A “load more” button that appends content via AJAX. Comments or reviews that render only after a click. Content behind a form submission. In all of these the content does not exist in the DOM until an interaction the renderer never performs, so it is never captured and never indexed.
The distinction is not “is the content visible” — it is “is the content in the DOM at load.” A correctly built progressive-disclosure UI keeps the content in the DOM and uses CSS or ARIA to manage visibility; a content-invisibility bug fetches the content on demand. The two look identical to a user and opposite to a crawler.
The practical test: load the page, open DevTools, and search the rendered DOM (not the source) for a sentence from the hidden content without clicking anything. If it is there, Google can index it. If the sentence only appears in the DOM after you click the tab, Google cannot. Apply this to every progressive-disclosure pattern on pages that need to rank — FAQ accordions, tabbed specifications, lazy-loaded article bodies — because each is a candidate for content that exists for users and not for crawlers.
Detecting these defects means comparing the raw HTML against the rendered DOM and flagging exactly where they diverge — which is laborious to do by hand across a site and impossible to eyeball, because the broken state is transient or hidden. A technical-SEO platform that captures both the raw HTML snapshot and the post-render DOM for every page can diff them and surface the specific divergences that break indexing: a canonical or robots directive that changes during hydration, primary content present in the rendered DOM but absent from the raw HTML (rendering-dependent and at risk if rendering fails), and content that exists in neither because it only materializes on interaction. Reporting the exact tag that changed and the content block that went missing — with the raw value beside the rendered value — turns a class of bugs that no browser reveals into concrete, locatable defects.