WordPress Duplicate Canonical - How an AMP Plugin Killed My Indexing
I installed an AMP plugin and none of my pages got indexed. Not a few. None. Google Search Console showed zero valid URLs for weeks, and I couldn't work out why. The pages existed, the sitemap was clean, and there were no crawl errors. What there were, on every single page, were 2 canonical tags pointing at each other in a loop Google simply refused to resolve.
That's what a duplicate canonical does at its worst. It doesn't just split your link equity or confuse Google about which version of a URL to rank. It can wipe your indexing entirely. And it happens silently, with no warning, no error, and no obvious place to look.

Here's exactly how it happened.
WordPress core wrote a canonical tag when I published each page. Fine, 1 canonical, that's correct. Then I installed the AMP plugin. AMP pages are supposed to have a canonical pointing back to the non-AMP HTML version — that's by design, that's correct too. But the plugin also rewrote the canonical on the non-AMP version, pointing it at the AMP URL instead. Now both versions were pointing at each other. Google crawled the loop and indexed nothing.
That's the AMP version of the problem. But you don't need an AMP plugin to end up here.
Install an SEO plugin — Yoast, RankMath, it doesn't matter which. It writes its own canonical tag alongside the one WordPress core already output. Now you've got 2. Switch themes, or install a page builder, or add a schema plugin that injects meta into the head. Several of those also write canonicals. Not always. Not consistently. Just sometimes, on certain post types, on certain template files, when certain conditions are met. Now you've got 3. Maybe 4.
None of these plugins know about each other. None of them check whether a canonical already exists before writing one. They each do their job in isolation and assume they're the only one doing it.
Nobody owns the HEAD.
That's the architectural problem. WordPress has no canonical tag manager. There's no single function responsible for outputting 1 canonical. The head is a shared space, and every plugin, every theme, every widget that wants to write to it just writes. Core writes. Your SEO plugin writes. Your theme framework writes. The result is a that nobody fully controls and nobody fully audits.
Fixing it is whack-a-mole. You find the duplicate, you trace it to a plugin, you find the setting that suppresses it, you regenerate, you recheck Search Console, you find another one on a different template. You spend a Saturday on it. 3 months later, you install a new plugin, and it's back.
The "fix" that isn't a fix is adding remove_action() calls to your functions.php to suppress whatever's firing twice. That works until the plugin updates, renames the hook, or starts firing on a different priority. You're patching a system that wasn't designed to have 1 owner.
What CARL does.
CARL has 1 function that builds the canonical tag: buildCanonical(). It runs once, inside buildMetaBlock(), and it writes 1 tag. There's no SEO plugin writing a second one. There's no theme framework writing a third. The generated PHP file contains exactly the canonical URL you configured, and nothing else touches it.
There's no hook system for canonicals. There's no plugin ecosystem. There's no that 6 different authors write to. There's a function, a string, and an output. That's it.
Your canonicals are correct because there's no mechanism for them to be anything else.
Ready to build a site that can't be taken down by a student's plugin?
