JavaScript SEO: Raw HTML vs Rendered HTML in CSR, SSR and SSG

AI OVERVIEW

Google reads raw HTML first and renders later. Keep content, links, canonicals, robots tags and status codes in the server response.

A JavaScript page can look finished in your browser while the HTML your server actually sent contains almost nothing: an empty <div id="root"> and a script tag.

Google can usually fill that gap. It renders JavaScript. But rendering is a second step with its own rules, and those rules are where JavaScript SEO goes wrong. Whether a site uses React, Vue, or Next.js isn't the question. The question is what Google gets for each URL, and when.

How Google Processes a JavaScript Page

1. CrawlRaw HTML and HTTP status code
→
Render queueWaits for resources
→
2. RenderHeadless Chromium runs JS
→
3. IndexRendered HTML used for content and links

Two details in that pipeline cause most real-world problems:

  • The raw HTML is read before renderingSome decisions, like whether a page is noindexed, can be made at that point, before any JavaScript runs.
  • Rendering can happen later than crawlingMost pages render quickly, but content and links that only exist after JavaScript runs aren't available until then.
Key point

JavaScript isn't bad for SEO. Pages break when important content, links, or signals exist only after JavaScript runs, and the JavaScript fails, depends on something Googlebot doesn't have, or contradicts the raw HTML.

CSR, SSR, SSG: Where the HTML Comes From

CSR SSR SSG Server sends a shell Server builds per request Built ahead of time <div id="root"> empty until JS runs Raw HTML: nearly empty Raw HTML: full page Raw HTML: full page
What a crawler receives before any JavaScript runs.
  • Client-side rendering (CSR)The server sends a shell and the browser builds the page. Everything a search engine cares about depends on app.js running and its API calls succeeding. That's a lot of dependencies for a blog post or product page.
  • Server-side rendering (SSR)The response already contains the heading, content, and links. JavaScript then hydrates the page, attaching interactivity. Client code can still overwrite titles, swap canonicals, or replace content after hydration.
  • Static site generation (SSG)HTML is built ahead of time and served as files. It suits blog posts, docs, and marketing pages. Same caveat as SSR: client JavaScript can still change it.
  • Hybrid sitesMost frameworks mix these per route: SSG for the blog, SSR for products, CSR for the account area. So ask "is this CSR?" per template, not per site.
<body>
  <div id="root"></div>
  <script src="/app.js"></script>
</body>

Things That Break JavaScript Pages in Google

  • A noindex in the raw HTMLGoogle's documentation says that when it finds a noindex robots meta tag in the raw HTML, it may skip rendering entirely. "Ship noindex in the shell, remove it with JavaScript later" can keep the page out of the index.
  • Wrong status codes in single-page appsSPAs often return 200 OK for every route, including ones that don't exist, while JavaScript shows "not found." Google tends to treat these as soft 404s. Its suggested fixes: redirect with JavaScript to a URL that returns a real 404, or add <meta name="robots" content="noindex"> with JavaScript on error pages.
  • Routes built with fragmentsGoogle ignores everything after #, so /#/products/wallets isn't a separate page. Use the History API so each view has a real path like /products/wallets that returns content when requested directly.
  • Content that needs a click or a scrollGooglebot doesn't click buttons, open tabs, or scroll like a user. Native loading="lazy" or an IntersectionObserver is fine. A "Load more" button is not.
  • Content that depends on stored stateGooglebot is stateless. It clears cookies, localStorage, and sessionStorage between page loads, so content gated on a saved preference or consent choice may render as an empty fallback.
  • Links that aren't linksRendered links still need to be <a href="/real-url">. Navigation built on onclick handlers or <div> elements gives Google nothing to follow.
  • Metadata that changes after renderingGoogle generally uses the rendered title, but canonicals are riskier. If raw HTML has one canonical and JavaScript sets another, you're sending conflicting signals. Set it correctly in the raw HTML.
  • Failed requests during renderingA timed-out API, a script blocked by robots.txt, a CORS error, or an endpoint that requires login all leave the rendered page incomplete. Your own session and cache can hide this in the browser.
  • Stale cached JavaScriptGoogle caches resources aggressively. Fingerprinted file names like app.3f9a2c.js make sure a new deploy is picked up as a new file.
Fragment route

/#/products/wallets
Google ignores everything after #

History API route

/products/wallets
A real URL that returns content directly

A Note on Dynamic Rendering

Dynamic rendering means serving pre-rendered HTML to bots and the JavaScript app to users. Google now describes it as a workaround, not a recommended solution. SSR or SSG fixes the same problem without maintaining two versions of every page.

Not Every Crawler Renders

Google's rendering is unusually capable. Many other bots, including a number of AI crawlers, appear to read only the raw HTML. If content is in the raw response, every crawler can see it. If it only exists after rendering, you're relying on each bot to run your JavaScript. We cover the differences in our guide to AI crawlers vs traditional search crawlers.

How to Test Raw vs Rendered HTML

  • Look at the raw responseUse View Source (not Inspect, which shows the live DOM) or fetch the URL from the command line. Note the title, meta robots, canonical, H1, main content, internal links, and structured data.
  • Look at what Google renderedIn Search Console, the URL Inspection tool shows the rendered HTML plus any resources Google couldn't load. The Rich Results Test does the same for any public URL.
  • Compare the signals, not just the visualsUse the table below as your diff list.
  • Request deep URLs directlyPaste a deep route into a fresh private window without clicking through from the homepage. Routes that only work after the app has loaded once are broken for crawlers.
  • Test one URL per templateProduct, category, blog, and filtered listing pages share code. A problem in one usually means a problem in all of them.
ElementWhat to watch for between raw and rendered
Meta robotsnoindex in raw HTML that JavaScript later removes
TitlePlaceholder or identical titles across routes in the raw HTML
CanonicalMissing in raw, different in rendered, or duplicated
H1 and main contentPresent only after rendering, or missing after a failed request
Internal linksOnly in rendered HTML, or not real <a href> elements
Structured dataInjected by JavaScript with values that don't match the page
Status code200 returned for routes that show "not found"

Checking Rendered Pages at Scale

Manual checks work for a handful of URLs. For a whole site, you need a crawler that renders.

The SiteAuditLint JavaScript rendering feature crawls pages with Chromium, so titles, headings, canonicals, and links are read from the page a browser actually builds. Run the crawl with rendering and look for templates where titles repeat, H1s are missing, or link counts drop to near zero. Then run those URLs through the URL Inspection tool to confirm what Google saw.

[Add screenshot here: a SiteAuditLint crawl of a JavaScript site with rendering enabled, ideally showing a template with missing H1s or duplicate titles.]

The Short Version

CSR, SSR, and SSG are all fine choices when the URL that matters returns the right status, the right signals in the raw HTML, and the right content after rendering. Check both versions of the page, and fix whatever changes between them that you didn't intend.

Use JavaScript for interactivity. Put content, links, canonicals, robots directives, and the correct status code in the HTML your server sends.