SEO for JavaScript-rendered sites

How rendering is handled, the difference between SSR, prerendering and client-side rendering, hydration and SPA routing, and how to see what the crawler actually receives.

The rendering options

ApproachHTML on first responseBest forMain risk
Static generationComplete, prebuiltContent that changes on a scheduleRebuild latency for large sites
Server-side renderingComplete, generated per requestPersonalised or fast-changing contentOrigin latency and cache correctness
Incremental static with revalidationComplete, cached, refreshed in backgroundLarge catalogues and content sitesStale page served after an update
PrerenderingInjected for crawlers, client-rendered for usersLegacy SPAs you cannot rewrite yetUser and crawler see different pages
Client-side rendering onlyEmpty shell plus a scriptLogged-in app screensDelayed indexing and missing content
# what does a crawler actually receive? ask as a plain client, no JS execution
curl -sL -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
  https://example.com/guides/dns/ttl | head -60

# and then compare with a rendered view
# 1. open the URL in a browser with JavaScript disabled
# 2. check the rendered HTML in the browser devtools after load
# 3. compare the two - anything only in the second is at risk
  • If the title, description, canonical and main content are absent from the raw response, indexing depends entirely on the rendering queue and will be slower and less reliable.
  • Check the noscript path. A page that renders nothing without JavaScript is a page whose content an engine may never see.
  • Server-render metadata at minimum: title, meta description, canonical, hreflang and structured data.

SPA routing and hydration

// metadata per route: the crawler needs it in the served HTML, not set on load
// server side, before sending the response:
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(renderToString({
  title: "DNS TTL and caching explained",
  description: "What TTL controls at each cache layer and how to plan a cutover.",
  canonical: "https://example.com/guides/dns/ttl"
}));

// client side, keep the router honest
router.afterEach((to) => {
  document.title = to.meta.title;
  const link = document.querySelector('link[rel="canonical"]');
  if (link) link.href = to.meta.canonical;
});
  1. Serve every route from the server with a real status code. A SPA that returns 200 for a nonexistent route makes every broken URL look like a valid page.
  2. Return a real 404 for unknown routes rather than a client-side empty state - otherwise the crawl fills with soft 404s.
  3. Hydration must produce the same tree the server rendered. A mismatch causes a re-render and can shift the main content after the crawler has read it.
  4. Avoid rendering the main content behind an interaction. Tabs, accordions and lazy sections that only exist after a click are not reliably indexed.
  5. Infinite scroll needs equivalent paginated URLs. Links that only exist after scrolling are links the crawler may never follow.

Testing the rendered output

Weekly check, five minutes

1. curl the URL without JavaScript       -> is the content there?
2. Inspect with JavaScript disabled      -> does the page work at all?
3. Search Console URL inspection         -> does the rendered HTML match?
4. Block the script in devtools          -> is a fallback or message shown?
5. Check the canonical in both views     -> same URL, no client-side override
SymptomLikely causeFix
Content indexed only after weeksClient-side rendering with no SSRServer-render the content or prerender
Title shows the site name on every pageTitle set only after hydrationEmit the title in the served HTML
Duplicate pages from query routesNo canonical per routeRender the canonical server-side
Soft 404s in coverage reportsSPA returns 200 for missing routesReturn 404 for unknown routes
Internal links not followedLinks built on click, not as anchorsRender real anchor elements with href
⚠️
Injection or prerendering that serves different content to a crawler than to a user is cloaking, even when the intent is innocent. If the prerendered page and the user's page differ in substance, you are relying on the crawler never comparing them - which is not a durable position.

FAQ

Is client-side rendering still a problem?
Rendering is handled for most sites now, but it is queued and slower, and anything that requires interaction to appear is unreliable. Server-rendering the content and metadata removes the whole class of problem.
Do I need a prerender service?
Only as a stopgap for a legacy SPA. It adds a component that can serve stale or inconsistent pages. Migrating to server rendering is the durable fix.

Technical SEO and measuring it Page speed and Core Web Vitals optimisation

Last refreshed 2026-09-18.