Canonical Guard
Finds WordPress pages that quietly tell Google they are some other page. Reads the rendered HTML, not the SEO plugin's settings.
by Marc Cangiano · github.com/marccangiano/canonical-guard · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/marccangiano/canonical-guard/archive/refs/heads/main.zipReadme
Canonical Guard
Finds pages that quietly tell Google they are some other page.
Why
Across 68 production sites, every blog post was rendering the blog archive's SEO metadata. Every article on every one of those sites was telling Google it was the same URL.
Nothing in the CMS looked wrong. The SEO plugin's settings were correct on every post. No error was thrown, no log line appeared, and no monitor went red. Those clients had been paying for content that could not rank, and nobody knew, because the fault existed only in the bytes the server sent to the browser.
That's the failure this plugin looks for, and it's why the scanner works the way it does.
It reads the rendered HTML, deliberately
Asking the SEO plugin what the canonical should be is easier and useless. The bug is a theme or template defect that happens after the SEO plugin has done its job correctly. The stored value is right. The output is wrong.
So Canonical Guard fetches each published URL over HTTP and parses the canonical out of the response body, with DOMDocument rather than a regex. A regex over <head> works until it meets a canonical inside an HTML comment or a link tag in a script string, and then it reports the wrong URL with total confidence.
What it reports
| Issue | Meaning |
|---|---|
collision |
Several URLs declare the same canonical. Only one of them can rank. This is the headline case. |
foreign |
A page's canonical points somewhere other than itself. Sometimes intended, often a template leaking another page's metadata. |
missing |
No canonical tag in the rendered output at all. |
fetch_failed |
The page could not be fetched, reported rather than skipped. A scanner that silently drops the pages it couldn't read has the same disease it's diagnosing. |
Use
From the admin, Tools → Canonical Guard → Scan the site.
From the command line, which is how you run it across a portfolio:
wp canonical-guard scan
wp canonical-guard scan --limit=50 --format=json
It exits non-zero when it finds something, so a loop over many sites can gate on it. A checker that always exits 0 gets wired into cron and then ignored.
Testing
The test suite is built on wp-e2e-kit and runs against a real WordPress in CI.
The interesting part is the fixture. tests/fixtures/cg-break-canonical is an mu-plugin that makes any post slugged cg-broken-* emit the home page as its canonical, which is the exact shape of the original defect. Posts slugged cg-clean-* get WordPress's correct default.
One scan therefore proves both halves: the scanner finds the three colliding posts, and it leaves the clean ones alone. A test that only checked the broken case could not tell a working scanner from one that flags everything.
npm install
npx playwright install chromium
npm run env:start # needs Docker
npm run test:e2e
Two things CI does that are easy to get wrong, both learned the hard way:
- Pretty permalinks have to be enabled. WordPress only serves
/wp-json/when the permalink structure isn't Plain, and Plain is what a fresh install gets. Otherwise every REST route 404s, core ones included, and it looks like a broken auth layer. wp-env run tests-cli, notwp-env run cli. wp-env runs two WordPress instances, dev on 8888 and tests on 8889. The suite talks to 8889.
Scope
Published posts and pages. It does not scan archives, taxonomy terms, paginated pages or attachments yet, and it does not attempt to fix anything. Reporting a fault accurately is the whole job; a plugin that silently rewrites canonicals across a client site is a worse problem than the one it solves.
License
MIT