JavaScript SEO: how to check the server HTML matches what renders

Short answer

Every page has two versions: the server HTML that arrives first, and the rendered page after JavaScript has run. Google reads the server HTML first and the rendered page later, once it has had time to render it. Many other crawlers, including the major AI crawlers when last studied, only read the first. If your title, description, canonical or robots tag only appears after JavaScript runs, those crawlers see something else. To check, compare the page source with the rendered page, or run the snippet below.

Every page has two versions

A site built with a JavaScript framework often sends a near-empty shell, then fills in the page in the browser. It looks perfect on screen. Here's what a crawler that doesn't run JavaScript gets:

Server HTML what arrives first
<head>
  <title>Example Store</title>
  <link rel="canonical"
    href="https://example.com/">
  <script src="/app.js"></script>
</head>
<body>
  <div id="root"></div> ← empty
</body>
Rendered page after JavaScript runs
<head>
  <title>Blue Widgets | Example Store</title>
  <link rel="canonical"
    href="https://example.com/widgets/blue">
  <meta name="description" …>
</head>
<body>
  <h1>Blue Widgets</h1> …
</body>

To a crawler that skips JavaScript, every product page on this site has the same title and a canonical pointing at the homepage. It looks like thousands of copies of one page.

Who sees which version

Google handles JavaScript pages in three phases: it crawls the server HTML, queues the page for rendering, then indexes what rendered. Google says pages may sit in that queue for a few seconds, or longer. Many other crawlers don't render at all:

WhoReadsNotes
GooglebotServer HTML, then renderedRendering can be delayed. A noindex in the server HTML can stop it rendering at all.
Google's Gemini, ApplebotRenderedGemini relies on Googlebot's crawling and rendering. Applebot renders with a browser-based crawler.
GPTBot, ClaudeBot, PerplexityBotServer HTML only (Dec 2024)A Vercel study of AI crawlers (December 2024) found none of the major ones ran JavaScript.
Link previewsServer HTML, usuallySlack, LinkedIn and Facebook previews generally read the tags the server sends.

So "Google renders JavaScript" is true, but it's no longer the whole question. If you want a page described properly in AI answers and link previews, its key tags have to be in the server HTML.

What's safe to set with JavaScript

Title and description Safe for Google. Google says you can set or change them with JavaScript. Crawlers that don't render still see the server version, so make that a sensible fallback, not a generic site name.
Canonical Risky. Google says not to use JavaScript to change the canonical to something different from the server HTML. A shell that ships the homepage as every page's canonical is the classic mistake.
Robots noindex One direction only. Adding noindex with JavaScript works for Google, which suggests it for error pages. Removing a noindex the server sent may not, because Google can skip rendering a page whose server HTML says noindex.
Main heading and content Fine for Google, invisible to the rest. If the H1 and body copy only exist after rendering, crawlers that skip JavaScript see an empty page.
Links Must be real links. Google only discovers links that are <a> elements with an href. A button or onclick that changes the page isn't followed.

How to check by hand

1Compare both versions with a snippet 1 minute

Open DevTools (F12), go to the Console and paste:

// Compares the server HTML with the rendered page for the tags crawlers care about
const res = await fetch(location.href, { cache: 'no-store', headers: { Accept: 'text/html' } });
const server = new DOMParser().parseFromString(await res.text(), 'text/html');
// Only noindex and nofollow matter for robots; dropping a harmless "index, follow" changes nothing.
const restrictive = doc => [...doc.querySelectorAll('meta[name="robots" i], meta[name="googlebot" i]')]
  .flatMap(m => (m.getAttribute('content') || '').toLowerCase().split(/[\s,]+/))
  .flatMap(t => t === 'none' ? ['noindex', 'nofollow'] : [t])
  .filter((t, i, all) => (t === 'noindex' || t === 'nofollow') && all.indexOf(t) === i).sort().join(', ');
const read = doc => ({
  title: doc.title.trim(),
  description: doc.querySelector('meta[name="description" i]')?.getAttribute('content')?.trim() || '',
  canonical: doc.querySelector('link[rel="canonical" i]')?.href || '',
  robots: restrictive(doc),
  h1: doc.querySelector('h1')?.textContent.replace(/\s+/g, ' ').trim() || '',
});
const a = read(server), b = read(document);
console.table(Object.keys(a).map(field => ({
  field,
  'server HTML': a[field] || '(none)',
  'rendered page': b[field] || '(none)',
  match: a[field].toLowerCase() === b[field].toLowerCase() ? 'same' : 'DIFFERENT',
})));

It fetches the page's HTML again, reads it without running any scripts, and lines it up against the page in front of you: title, description, canonical, robots and H1, each marked same or DIFFERENT.

It compares the first title, description, canonical and H1, and the noindex and nofollow rules from every robots tag. It sees what your browser is sent: a site that serves Googlebot different HTML won't show that here.

2Turn JavaScript off and look 2 minutes

  1. Open DevTools and press Ctrl+Shift+P (⌘+Shift+P on a Mac).
  2. Type Disable JavaScript, press Enter, then reload the page.
  3. What you see now is roughly what a crawler that skips JavaScript gets. Check the tab title, the heading and whether the content and links are there.
  4. Run Enable JavaScript the same way when you're done. It only lasts while DevTools is open.

3See what Googlebot rendered 3 minutes

In Google Search Console, paste the URL into URL Inspection, choose Test live URL, then View tested page. The HTML tab shows the page as Googlebot rendered it, and the screenshot shows whether the content made it in. Search inside the HTML for your title and canonical.

How to fix it

  1. Put each page's own tags in the server HTML. Server-side rendering or static generation does this. Most frameworks set the title, description and canonical per page during the server render. Dynamic rendering (a separate version for bots) is a workaround Google doesn't recommend long term.
  2. Fix the shell's defaults if you can't render on the server yet. A missing canonical is better than one that points every page at the homepage.
  3. Never ship noindex expecting JavaScript to remove it. Decide indexing in the server HTML.
  4. Use real links for navigation and pagination: <a href="/page-2">, with JavaScript on top if you like.
  5. Handle "not found" properly in single-page apps. Google suggests either sending the visitor to a URL that returns a real 404, or adding a noindex robots tag to the error view.

How crawlers report this

If a crawl sent you here, these are the labels to look for. Our guide to Screaming Frog and Sitebulb issues covers other common ones.

Screaming FrogSitebulbWhat it means
JavaScript: Noindex Only in Original HTMLNoindex only in the HTTP response HTMLThe server sends noindex and JavaScript removes it. Google may skip rendering, so the noindex can stand.
JavaScript: Page Title Only in Rendered HTMLPage Title only in the rendered HTMLThe server HTML has no title, so it depends on rendering.
JavaScript: Page Title Updated by JavaScriptPage Title modified by JavaScriptThe title changes after load.
JavaScript: H1 Only in Rendered HTML<h1> only in the rendered HTMLThe main heading appears only after JavaScript runs.
JavaScript: Canonical MismatchCanonical mismatch between rendered and response HTMLThe canonical changes after load, which sends mixed signals.
JavaScript: Canonical Only in Rendered HTMLCanonical only in the rendered HTMLThe canonical is added by JavaScript, not sent by the server.

Doing this on every page?

SEOSnip compares both versions for you

Open the extension on any page and it reads the server HTML alongside the rendered page, comparing the title, description, canonical, H1 and robots directives. You get:

  • Each field that JavaScript adds, removes or changes, side by side
  • Marked as an error when the title, canonical or robots rules change, and a warning when only the description or H1 differs
  • A copyable snippet of what the server HTML should contain
  • A ticket with that evidence, and a "Check if fixed" button that re-runs the comparison