squirrelscan
GitHub

Link rel=canonical header vs the HTML tag: the mismatch

A page declaring one canonical in an HTTP header and a different one in the markup. squirrel parses the header and compares.

A canonical can be declared in an HTTP header as well as in the markup: Link: <https://example.com/page>; rel="canonical". The header exists for responses that have no HTML to put a tag in, which is what makes it the right way to canonicalise a PDF or an image.

For an HTML page there is no reason to use both. When a page sends both and they disagree, a search engine has two contradictory declarations from the same page and has to pick, which is exactly the ambiguity the canonical was meant to remove.

What squirrel checks

The rule runs on every crawled page. It parses the Link response header, splitting on commas and matching entries of the form <url>; rel=canonical, with the rel value optionally quoted and matched case-insensitively. It emits one check named canonical-header:

  • Skipped with the message No Link header canonical found and the reason Page has no HTTP Link canonical header. This is the normal case.
  • At info status with the message Canonical only in Link header (no HTML tag) when the header declares one and the markup does not, which is valid.
  • Passing with the message HTML canonical matches Link header when both are present and agree.
  • Failing with the message Canonical mismatch between HTML and Link header, the value HTML: <url>, Header: <url> and the expectation Both should match.

The comparison normalises both URLs first: the fragment is dropped, a trailing slash is removed from the end of the whole serialised URL except on the site root, and the string is lowercased. Because the trim is on the serialised form rather than the path, https://example.com/page/?x=1 keeps its path slash. So https://example.com/Page/ in the header and https://example.com/page in the markup are treated as agreeing.

The comma split is a simplification of the Link header grammar. A URL containing a literal comma can be split across two entries, though a comma in a path is rare enough that it does not come up in practice.

How to fix it

Pick one. For an HTML page, that is the tag:

<link rel="canonical" href="https://example.com/guides/build-cache" />

Then stop sending the header, which usually means removing a rule at the CDN or reverse proxy that was added for a different content type and applied too broadly.

The common false positive is a CDN adding the header automatically with the request URL, on a page whose markup canonicalises somewhere else deliberately. The mismatch is real and the fix is to stop the CDN adding it.

Rule IDcore/canonical-header
CategoryCore SEO
ScopePer-page
Severitywarning
Weight7/10

Enable / disable

Disable this rule

[rules]
disable = ["core/canonical-header"]

Disable all Core SEO rules

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

Enable only this rule

[rules]
enable = ["core/canonical-header"]

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.

Core SEO findings ship in every audit next to the performance, security and agent experience rules. See Fix SEO issues with AI for how an agent works through a report.

References

Check your site

Run squirrel audit https://example.com and open the Core SEO section of the report. Each mismatching page shows both declared values. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.

Type to search…

↑↓ navigate↵ openesc close