squirrelscan
GitHub

Rendered signal mismatch: JavaScript changes the canonical, noindex, title or H1

A script that rewrites the canonical, adds or removes a robots noindex, or replaces the title or first H1 makes crawlers that read raw HTML see a different page from a browser.

What it is

Crawlers and agents that do not run JavaScript read the raw HTML the server sends. A browser shows the page after scripts have run. When a script rewrites the canonical URL, adds or removes a robots noindex, or replaces the title or first <h1>, the two readers get different pages.

The signals that matter most are the canonical and noindex. A canonical or noindex that differs between the raw and the rendered page is a common cause of pages dropping out of the index after a framework migration: the server sends one thing, the client script sends another, and which one a search engine acts on depends on whether it renders the page.

ax/content-without-js already compares the body content of the raw and the rendered page. This rule compares these four head signals.

What squirrel checks

It compares the raw HTML the crawl fetched with the DOM from the cloud render of the same page, and emits four checks:

CheckFires whenSeverity
canonical-mismatchthe canonical URL differs, or the script removes it or adds one that points elsewhereerror
noindex-mismatchnoindex is present on only one side, in any robots or googlebot meta tagerror
title-modifiedthe title text differs after whitespace and case are normalisedwarning
h1-modifiedthe first H1 differs, or exists on only one sidewarning
  • Canonical URLs are resolved against the page before they are compared, so a relative canonical equals the same absolute one. A trailing slash, the case of the host, a default port (:443) and a #fragment do not count as a difference. A different path, query, scheme or host does.
  • A raw page with no canonical where the script adds one that is the page’s own URL is a warning, not an error. That is the usual shape of a single-page app: crawlers that do not run JavaScript pick a canonical themselves, and the one the script adds is the right one. A canonical the script adds that points anywhere else, one it changes, and one it removes still fail.
  • noindex is read from every <meta name="robots"> and <meta name="googlebot"> tag on the page, on both sides, with the name matched without regard to case; the page is noindex when any of them says noindex or none, as a token in the comma-separated list. A script that appends a second tag, so that the first still reads index, follow, is a mismatch. The finding says which side carries it: raw or rendered.
  • Only the first <h1> is compared.
  • Titles and H1s compare equal when only whitespace or letter case differs. A finding shows both texts, cut at 200 characters.

A page whose signals are the same on both sides passes all four checks.

When it does not run

The rule reports one skipped check, named rendered-signal-mismatch, with the reason, and never a pass or a fail, when:

  • the crawl browser-rendered the page, because the raw and the rendered HTML are then the same document. Run an audit that does not render the page to compare them;
  • there is no render result for the page: a CLI audit without the cloud, --offline, a render that was disabled, or one that returned no HTML;
  • the page is a soft 404, because both sides are then the error shell.

Cost

The rule reads the same cloud render that ax/content-without-js reads, which runs inside the audit and is billed as the page’s own audit charge. It adds no credits.

How to fix it

Put the canonical and the robots meta in the HTML the server sends, and make client code leave them alone.

<head>
  <title>Pricing</title>
  <link rel="canonical" href="https://example.com/pricing">
  <meta name="robots" content="index, follow">
</head>

In a framework that sets head tags on the client (a head manager, a router hook), move those tags into the server render, or into a static page, so they are in the first response. Keep the title and the first <h1> in the server HTML for the same reason: what a crawler reads should be what a visitor sees, not a placeholder such as Loading... that a script replaces.

Rule IDcrawl/rendered-signal-mismatch
CategoryCrawlability
ScopePer-page
Severityerror
Weight6/10

Enable / disable

Disable this rule

[rules]
disable = ["crawl/rendered-signal-mismatch"]

Disable all Crawlability rules

[rules]
disable = ["crawl/*"]

Enable only this rule

[rules]
enable = ["crawl/rendered-signal-mismatch"]

enable replaces the default ["*"], so this is the only rule selected. Leave disable out: a rule that matches any disable pattern is skipped even when enable names it. Only an enabled setting under a rule’s own [rule_options] table overrides both lists.

Type to search…

↑↓ navigate↵ openesc close