squirrelscan
GitHub

Canonicals in two forms: www, scheme and trailing slash

Half the site canonicalising to www and half to the apex domain. squirrel compares four form axes across the crawl.

What canonical form drift is

Every canonical on the site can be individually valid and the set can still be wrong. If half the pages canonicalise to https://www.example.com/thing/ and half to https://example.com/thing, the two halves consolidate onto different hosts and the ranking signal splits between them.

No per-page rule can see this, because a page only knows its own canonical. Four things are one decision for a whole site: the scheme, whether the host carries www, whether tracking parameters survive into the canonical, and, within a section, whether paths end in a slash.

What squirrel checks

The rule runs once per crawl and emits a single check named canonical-form-drift. It compares four axes: scheme, host, trailing slash and tracking params.

  • Skipped with the message Only N page(s) crawled; 10 needed to establish a canonical-form norm.
  • Skipped with the message No canonical form is shared by 70% of at least 10 comparable pages.
  • Skipped with the message No pages available for analysis when the crawl produced no pages. That wording comes from the non-streaming path; a streaming crawl with no pages takes the Only 0 page(s) crawled branch instead.
  • Passing with the message All N comparable canonical(s) share one form (scheme https, host apex) when nothing drifts. The summary lists up to ten judged axes in a fixed order, scheme then host then trailing slash then tracking params, each as the axis name, its section where it has one, and the winning form.
  • Warning, or failing, with the message N of M canonical(s) are declared in a different form than the rest of the site (scheme https, host apex). Each item reads 40 of 45 canonicals in /blog use trailing slash, these 5 use no trailing slash and carries up to 20 example URLs. The status is fail when the deviant pages are 20% or more of the judged pages, and warn below.

A page enters the comparison only when its status was 2xx and its canonical is present, absolute and points at the same hostname as the page once a leading www. is stripped from both. A canonical pointing at a sibling subdomain is treated as cross-host and dropped. Missing, relative, unparseable and cross-host canonicals all belong to core/canonical and are dropped here rather than reported twice.

The trailing-slash axis is compared inside a section rather than site-wide, because using a slash in /blog and not in /product is a normal pattern. The section is the first path segment, or / for a flat path, and each section has to clear the sample floor and the agreement bar on its own. The site root and any path whose last segment contains a dot are excluded from that axis entirely, since neither had a choice.

Tracking parameters are matched by name: _ga, fbclid, gclid, igshid, mkt_tok, msclkid, ref, source, ttclid, yclid, and anything starting utm_ or mc_. Having a query string at all is not the test, because a paginated canonical legitimately carries one.

How to fix it

Build the canonical from one function that everything calls, rather than from a template string per route:

const canonical = new URL(path, "https://example.com").href;

That fixes scheme and host by construction. For the trailing slash, pick one convention per section and let the router enforce it. For tracking parameters, strip them before building the canonical rather than after.

The common false positive is a site mid-migration, where the new form is correct and the old pages have not caught up. The finding is accurate and the fix is the migration.

Rule IDcore/canonical-form-drift
CategoryCore SEO
ScopeSite-wide
Severitywarning
Weight3/10

Enable / disable

Disable this rule

[rules]
disable = ["core/canonical-form-drift"]

Disable all Core SEO rules

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

Enable only this rule

[rules]
enable = ["core/canonical-form-drift"]

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 drifting axis is listed with the norm, the deviant form and the URLs. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.

Type to search…

↑↓ navigate↵ openesc close