all posts
2026-06-16

Technical SEO for React and Vite SPAs Googlebot actually rewards

Googlebot runs JavaScript. That sentence has cost more teams their rankings than almost any other in SEO, because "can render" and "reliably indexes" are two very different promises — and the gap between them is exactly where a React single-page app leaks traffic.


If you ship a client-rendered Vite SPA, your homepage is, to a first approximation, an empty <div id="root"> and a script tag. A human browser fills it in instantly. A crawler has to do more work — and whether it bothers, and how long it waits, is the whole game. Let's be precise about what actually happens to a React SPA in Google's pipeline, and which fixes move the needle versus which are cargo-cult.

The real problem: two waves of indexing

Google doesn't index a JavaScript page in one pass. It does it in two, and Martin Splitt from Google's Search Relations team has been explicit about why you shouldn't lean on the second one: "Even though Googlebot can render JavaScript, we don't want to rely on that."

Here's the pipeline. Wave one is immediate: Googlebot fetches your URL and parses the raw HTML that came back over the wire — before a single line of your application JavaScript runs. Wave two is rendering: the page goes into a render queue, a headless Chromium eventually executes your JS, and the resulting DOM is parsed for content and links. That second wave is queued and can land anywhere from a few hours to a few weeks after the first.

crawl wave 1 — parse raw HTML (immediate) empty shell ← no content, no links render queue (hours → weeks) waiting for headless Chromium wave 2 — render & index content found finally day 0 days–weeks later
For a pure client-rendered SPA, wave one finds an empty shell — no copy to index and no links to follow — and everything real waits on a render queue you don't control. Prerendering moves the content into wave one, where indexing is fast and reliable.

The consequence is brutal and quiet: for a pure client-rendered app, wave one sees no copy worth indexing and, worse, no links to follow — so the crawler can't even discover your other pages until rendering catches up. You don't get an error in Search Console. You just get slow, partial, occasionally-stale indexing, and you blame the algorithm.

The fix that actually matters: get content into wave one

Every other tactic in this post is a rounding error next to this one. Put real HTML in the initial response so wave one has something to index. Google's own recommendation is server-side rendering, static rendering (SSG/prerendering), or hydration — in that order of preference over client-only rendering.

One option you can cross off: dynamic rendering (sniffing the user-agent and serving a pre-baked page to bots while users get the SPA). Google now explicitly calls it "a workaround and not a long-term solution" and no longer recommends it. It's brittle, it's a second code path that drifts out of sync, and it's a maintenance tax. Don't start there in 2026.

For a Vite + React app the pragmatic, low-cost answer is build-time prerendering: after your normal client build, render each public route's React tree to static HTML and bake the correct <head> (title, description, canonical, Open Graph) into the file Vite ships. The SPA still boots and hydrates for real users — they get the app — but the crawler gets a fully-formed document on the very first byte. Authenticated, interactive routes (a dashboard behind login) don't need any of this; search engines never see them, so leave them client-rendered.

This is exactly how this site is built. Public marketing and blog routes are prerendered to static HTML at build time with route-correct meta tags; the app itself hydrates on top. The dashboard stays a pure SPA. You get crawlable content where it matters and zero SSR runtime to operate.

The SPA-specific mistakes that silently tank you

Even with content in the initial HTML, single-page apps have a handful of failure modes that don't exist in a traditional multi-page site. These are the ones worth auditing:

MistakeWhy it kills rankingsThe fix
Hash routing (/#/pricing)Everything after # is invisible to crawlers — your whole app indexes as a single URLUse BrowserRouter (real paths) with a server catch-all
Soft 404sCatch-all routing returns HTTP 200 for URLs that don't exist; Google flags it as a soft 404 and wastes crawl budgetRender a real 404 view and return a 404 status
onClick navigationBots discover URLs from <a href>; a div with a click handler is a dead endEvery in-app link is a real anchor with href; intercept the click for speed, keep the href
Client-only meta tagsTitle/description set only after JS runs miss wave one entirelyBake per-route meta into the prerendered HTML
Content behind interactionCopy that only appears after a click/scroll/tab isn't in the rendered DOM Google indexesRender primary content on load, not on interaction
Blocked JS/CSSIf robots.txt blocks your bundles, wave two can't render the page at allLet Googlebot fetch your JS and CSS

None of these throw an error. That's what makes them dangerous — the app works perfectly for you in Chrome, so the regression hides until you notice an organic plateau three months later.

How to verify, in five minutes

Don't trust that it works. Check the thing Google actually sees:

  • View source, not Inspect. "View page source" is the raw HTML — wave one's input. If your content and links aren't there, neither is Google's. (DevTools "Inspect" shows the rendered DOM and will lie to you here.)
  • URL Inspection in Search Console. It shows the rendered HTML Googlebot produced and flags render errors. This is the ground truth for what got indexed.
  • Check <head> per route. Every indexable URL should have its own title, description, and canonical in the initial HTML — not the same homepage tags copy-pasted across the SPA.

The part most teams skip: confirm it in the rankings

Shipping prerendering or fixing hash routing is a deploy like any other, and its real test isn't a green check in a build log — it's whether the affected pages actually start getting crawled, indexed, and ranked. That feedback loop is long (the render queue and re-rank lag stack up to days or weeks), which is precisely why technical-SEO fixes get shipped, forgotten, and never credited with the traffic they earned.

Connecting a specific pull request — "switched to BrowserRouter," "added build-time prerendering" — to the ranking movement it eventually caused is exactly what Code Results does: changepoint detection on your Search Console history, matched back to the deploy that moved the number. Fix the rendering, then watch the page you fixed actually climb.


Sources: Google Search Central — JavaScript SEO basics & dynamic rendering, Search Engine Land — Google no longer recommends dynamic rendering, Botify — JavaScript rendering Q&A with Martin Splitt, Backlinko — JavaScript SEO best practices.