BlogFindings
Canonical tag not working: 35 of 1,057 home pages closed the head element early, and one lost its canonical
Google's documentation states that the canonical link element is only accepted inside the head section, and tells site owners to make sure at least the head section is valid HTML. Lantad read 1,057 corpus home pages on 13 November 2026: 35 carried an element the HTML parser does not permit in the head, 17 of those orphaned 172 declarations into the body, and exactly one of them was a canonical.
This post measures how often that actually happens, because the mechanism is well documented and its frequency is not. Every request below was sent on 13 November 2026 as LantadBot/1.0, the agent described on the bot page, with redirects followed, a twenty second timeout and no JavaScript executed. The corpus is the stratified sampling frame this repository commits for its crawlability study: 392 hostnames across ten platform strata and 1,027 across eight industry strata, 1,419 in total with no overlap between the two files. The answer turns out to be narrower than the mechanism suggests, and the narrowing is the useful part of it.
In short
- Lantad requested robots.txt and then the home page of all 1,419 hostnames in this repository's two committed corpus seed files on 13 November 2026, as LantadBot/1.0 with redirects followed and no JavaScript executed, and 1,057 of the 1,060 pages that answered with HTML carried a head element, holding 46,783 meta, link, title and base declarations between them.
- Canonical tag not working is a documented consequence of an invalid head: Google's canonicalization page, last updated 10 July 2026, states that the canonical link element is only accepted if it appears in the head section of the HTML, and adds that site owners should make sure at least the head section is valid HTML.
- 35 of those 1,057 home pages placed an element inside the head that the HTML Standard's in head insertion mode does not permit there, which closes the head and moves everything after it into the body. 16 of the 35 used a div, and 9 used a tag name no specification defines, among them APM_DO_NOT_TOUCH on isbank.com.tr and a misspelled m-eta on aiims.edu.
- 18 of the 35 pages lost nothing, because the offending element sat after every declaration in their head. The 17 that did lose something orphaned 172 declarations, and 88 of the 172 were asset or performance hints such as stylesheets and preloads rather than metadata an AI crawler reads.
- Chromium's own parser, run over the same bytes on 13 November 2026, placed exactly one canonical link element outside the head, on uhn.ca, and one title element outside it, on maxbone.com, where the document title still resolved because the HTML Standard processes a title start tag found in the body using the in head rules.
| Declaration | Still honoured outside the head? | Source, and the date the page carries |
|---|---|---|
| Canonical link element | no | Google, consolidate duplicate URLs, last updated 10 July 2026 |
| Robots meta tag | yes | Google, robots meta tag, last updated 24 March 2026 |
| JSON-LD structured data | yes | Google, intro to structured data, last updated 10 December 2025 |
| Title element | yes, still applied | HTML Standard, in body insertion mode, read 13 November 2026 |
What closes the head element before you meant it to
A parser does not read the head section as a region you drew with two tags. It runs a state machine, and the state that matters here is called the in head insertion mode. The HTML Standard lists what that state accepts: base, basefont, bgsound, link, meta, noscript, noframes, style, script, template and title, plus the head and html tags themselves. Those elements keep the parser where it is.
For anything else the specification is blunt. The in head insertion mode ends with a rule headed Anything else, and the rule is three steps: pop the current node, which will be the head element, off the stack of open elements, switch the insertion mode to after head, and reprocess the token. Read at html.spec.whatwg.org/multipage/parsing.html on 13 November 2026, where the standard carries a last updated date of the same day. Note what that sequence does and does not do. It does not skip the offending element or repair the document. It ends the head immediately before the element, so everything the author wrote afterwards belongs to the body, whatever the indentation in the template says.
MDN's reference for the head element states the same constraint in the vocabulary authors use rather than the one parser implementers use: the permitted content of head is one or more elements of metadata content, exactly one of which is a title. A div is not metadata content. Neither is a span, an img, an input, an anchor, a header or a nav. Neither is a custom element or a typo, because an unrecognised tag name is not a special case in the standard. It falls to the same Anything else rule as everything else.
None of this is a crawler doing something unusual, and that is the part worth holding on to. An AI crawler that reparents these nodes is not being strict or unforgiving. It is running the same algorithm every browser runs, and arriving at the same tree a browser arrives at. The document that reaches it is the document the bytes describe, not the document the template looks like, which is the same gap this blog measured when 85 meta elements on five pages turned out to include just two that addressed a crawler at all.
Flow: Parser in head mode (link, meta, title, script) to Metadata element; Parser in head mode (div, span, img, unknown) to Anything else; Metadata element to Stays in head; Anything else to Pop the head element; Pop the head element to Switch to after head; Switch to after head to Reprocess the token; Reprocess the token to Rest parsed into body.
Why is a canonical tag not working when it sits in the head you wrote?
Because the head you wrote and the head the parser built are different objects, and only one of them is the one Google reads. That is the whole of the mechanism, and the canonical link element is the declaration where it bites hardest, because Google's rule for it is positional in a way its other rules are not.
The contrast is worth laying out, since it is easy to assume an invalid head breaks everything in it and that assumption overstates the damage considerably. Google's page on the robots meta tag, last updated 24 March 2026, tells authors to place the tag in the head section and then says plainly that Google Search does not enforce that placement and will respect robots meta tags in the body section of an HTML document as well. Google's introduction to structured data, last updated 10 December 2025, describes JSON-LD as a notation embedded in a script tag in the head and body elements of a page, so a JSON-LD block that lands in the body is in a position the documentation already blesses. The title element survives too, and for a reason in the parser rather than in anybody's guidelines: the in body insertion mode handles a start tag named base, basefont, bgsound, link, meta, noframes, script, style, template or title by processing it using the rules for the in head insertion mode. The element is still created and still applied. It is simply parented somewhere else.
So of the four declarations most likely to be sitting in a head section, three tolerate being moved and one does not. The canonical is the one that does not, and Google's canonicalization documentation says so in the sentence this post opened with. That makes a broken head a canonical problem first and a structured data problem hardly at all, which is the opposite of how the defect is usually described.
Two earlier measurements on this blog give the baseline that this one sits against. 163 of 1,069 home pages carried no canonical at all and 63 named a different URL, and 629 of 1,023 carried no page level robots directive of any kind. Against numbers of that size, a tag that exists and is merely in the wrong half of the document is a smaller problem than a tag nobody wrote. It is also a harder one to see, because the source view of the page shows it exactly where it should be.
- Canonical link element Google: only accepted if it appears in the head section of the HTML
- Robots meta tag Google: does not enforce placement, respects the tag in the body as well
- JSON-LD structured data Google: documented for the head and body elements of a page
- Title element HTML Standard: a title start tag in body is processed with the in head rules
What Lantad measured across 1,419 corpus hostnames
Robots.txt first, for every hostname, because a page nobody is allowed to fetch is not a page to draw conclusions from. 32 of the 1,419 hostnames did not answer the robots request at all, and 1,107 answered it with a plain HTTP 200. Resolving the site root for the token LantadBot with the matcher in core/src/robots.ts left 1,155 hostnames allowed, 64 disallowed and 168 where the rules could not be determined. The 64 split into 52 hosts whose robots.txt returned a 5xx, which RFC 9309 section 2.3.1.4 says a crawler must read as a complete disallow, and 12 whose file parsed and disallowed LantadBot at the site root. The 168 are the hosts that refused the scanner's own request for the file, 162 with a 403, four with a 429 and two with a 401, so their rules were never seen and are reported as unknown rather than guessed in either direction. The reasoning behind that three way split, rather than a guess in either direction, is set out on the methodology page.
Of the 1,155 allowed hosts, one home page fetch failed outright and 1,060 answered HTTP 200 with an HTML content type. Three of the 1,060 served no head element at all, which is a different defect and not the one being counted here. That leaves 1,057 pages with a head section, and between them those heads held 46,783 declarations: 46,783 meta, link, title and base elements written by people who expected a machine to read them.
The test applied to each of the 1,057 was the parser's own rule and nothing more. Walk the head section from its start tag to its end tag, skipping the contents of script, style, template and noscript blocks because those are raw text or inert rather than markup the in head mode evaluates, and find the first start tag whose name is not on the standard's permitted list. That tag, if there is one, is where the head ends in the parsed document. Count the meta, link, title and base declarations the author placed after it.
One implementation detail is worth recording because it produced a wrong answer before it was found. The first run of this measurement lowercased each document before searching it for offsets, which is the obvious way to match tag names without regard to case and is wrong: lowercasing is length changing for some code points, and the Turkish capital I with a dot becomes two characters, so every offset after the first one drifted. Two pages were misread that way, one of them a Turkish bank. Case insensitive matching that preserves offsets fixed it. This is the same class of silent defect the robots.txt validator post found in 232 of 1,059 files, where the file parsed cleanly and meant something other than what it looked like, and it is why the scanner's own robots.txt tester reports what matched rather than only a verdict. A measurement is a program, and a program is wrong until it has been checked against something.
The elements 35 sites put inside their head element
A div, mostly. 16 of the 35 pages broke their head with a div, and the divs are not decorative: they are containers for things a build step injected. Trip.com opened one with the id midas-hidden-input, utah.gov one carrying an opaque identifier, workday.com one holding a marketing tag configuration, and nami.org one named for an advertising cookie. Three pages used a hidden input for the same purpose, two a span, two an img and one an anchor carrying a rel=me link to a Mastodon profile. One used a header element and one a nav, both of them real HTML elements that are simply not metadata content.
Nine of the 35 used a tag name that no specification defines, and these are the most legible ones because the name says who put it there. groundcover.com opened a vercel-speed-insights element, sundhed.dk an sdk-core-analytics, u.ae an sc-editing-context, scotiabank.com a bns-data element carrying a JSON blob of site configuration, and two local news sites, nowhabersham.com and matsusentinel.com, a broadstreet-zone element naming a numbered zone. isbank.com.tr opened an element literally named APM_DO_NOT_TOUCH. ocbc.com emitted the string that reads as a tag named sizes, which on inspection is a Java object printed into the markup by a template rather than an element anybody wrote. aiims.edu wrote m-eta, a one character typo of meta carrying a viewport declaration, which is the clearest case in the set: a single misplaced hyphen turns a valid metadata element into an unknown one and ends the head at exactly that point.
Platform matters less here than tooling does. The pages affected run on hosted commerce, on enterprise content management, on WordPress and on modern site builders, and the pattern they share is a third party snippet injected into the head by something that did not check what the head is allowed to contain. Shopify storefronts are represented, which is worth knowing if you work from the Shopify guide, and one site serves Vercel's analytics as a custom element inside its head. The per stack advice in the Next.js guide is aimed at the same class of problem: tooling that writes into the head without checking what is allowed to live there. The injected element is almost always invisible to a reader, which is why a template passes review: a hidden input, an analytics custom element and a display:none span all render as nothing.
The 172 declarations that ended up after one of these elements split almost evenly by purpose, and the split is the reason the finding is smaller than it first looks. 92 were link elements and 79 were meta elements, with a single title. By purpose, 88 of the 172 were asset and performance hints, among them 35 stylesheets, 21 preloads, nine dns-prefetch, eight preconnect and 12 icons. Those still work in the body, because a browser fetches a stylesheet it finds anywhere. The remaining 84 were descriptive metadata, and the largest groups there were 28 Open Graph properties across several pages, seven Google site verification tokens and four meta descriptions. The Open Graph properties bear on social preview rendering rather than on whether an answer engine can read the page, and a verification token has already done its job once the domain is verified. The one declaration in the set with a documented head-only requirement is the canonical, and it appeared once, which the title tag measurement and the schema validator measurement both prepare you for: defects in this layer are common, and the consequential ones are a small subset of them.
| Host | Element that closed the head | Declarations orphaned | Head-only loss |
|---|---|---|---|
| bokksu.com | span, display:none | 33 | none: description and Open Graph only |
| maxbone.com | img, 99999 by 99999 | 29 | title element |
| discover.com | input, type hidden | 20 | none: preloads and a description |
| groundcover.com | vercel-speed-insights | 16 | none: stylesheets and preloads |
| uhn.ca | span | 12 | canonical link element |
| visitmalta.com | div, id websiteInfo | 12 | none: verification and icons |
| jalan.net | div, id skyscraper | 10 | none: Open Graph and viewport |
| eatmila.com | div, data-rebuy-id | 9 | none: Open Graph image set |
| magicmind.com | img, 99999 by 99999 | 7 | none: stylesheets and prefetch |
| nami.org | div, ad settings | 7 | none: icons and viewport |
| nowhabersham.com | broadstreet-zone | 4 | none: icons only |
| japantimes.co.jp | input, chartbeat id | 4 | none: preconnect and stylesheets |
| matsusentinel.com | broadstreet-zone | 3 | none: verification and stylesheet |
| u.ae | sc-editing-context | 2 | none: two CMS meta elements |
| workday.com | div, id utag | 2 | none: stylesheets only |
| aiims.edu | m-eta, a typo of meta | 1 | none: one stylesheet |
| dynamoosejs.com | a, rel=me | 1 | none: one stylesheet |
What a real HTML parser did with those pages
Reading the bytes and applying the standard by hand is an inference, and an inference is worth checking against an implementation. So the same 35 pages were fetched again on 13 November 2026 and each document was handed to Chromium's own HTML parser, through DOMParser, with no network access and no scripts run. The question put to it was simply where each node ended up: inside the head element of the parsed tree, or outside it.
The answer confirmed the inference and then narrowed it further. 25 of the 35 pages had at least one meta or link element parsed outside the head, 87 meta elements and 192 link elements in total, although that aggregate includes elements genuinely authored in the body and so cannot all be charged to the head break. The per declaration counts are the unambiguous ones. Exactly one page had its canonical link element outside the head: uhn.ca, a hospital network, whose head opens a bare span element and then declares its canonical link element after it, confirmed on two separate reads the same day. Exactly one had its title element outside the head, maxbone.com, and four had a meta description outside it, three an Open Graph title and nine a JSON-LD block.
The title case is the one that changes the story, and it changes it in the product's disfavour. maxbone.com puts an img element with a width and height of 99999 inside its head, which closes it, and the title element that follows is parsed into the body. A reasonable prediction is that the page therefore has no title. Chromium reported a non-empty document title, the one the page's own markup declares. The title was applied, exactly as the in body insertion mode says it should be, which is why this post states that the title survives rather than guessing that it does not. The same holds for the JSON-LD, where 27 of 404 pages gained structured data only when a browser ran the page: the question is always what the parsed document contains, not what the template implies, and the only way to know is to parse it.
What remains, after the hints and the tolerated declarations are set aside, is one site whose canonical is not where its own markup appears to put it. uhn.ca names https://www.uhn.ca:443/ as its canonical URL, with the port written out, which is a second and separate problem: a canonical that a crawler does accept would still be pointing at a URL form the site does not otherwise serve. Whether Google treats that host and port as equivalent to the bare host is not something this measurement tested and no claim is made about it here. The underlying risk is the familiar one from 305 of 313 sites answering at more than one URL: a canonical is how a site tells a machine which address is the real one, and a canonical that is not read leaves that question open. Anyone wanting to see the parsed shape of their own page rather than their template's can do it with what GPTBot sees, and the broader point about what survives a fetch is the subject of two of nine crawlers rendering JavaScript at all.
uhn.ca: the canonical moved and matters
- bare span element opens inside the head
- canonical link element declared after it
- Chromium parsed the canonical into the body
- Google: only accepted inside the head section
- declared canonical was https://www.uhn.ca:443/
maxbone.com: the title moved and survived
- img element sized 99999 by 99999 in head
- title element parsed into the body
- document title still resolved in Chromium
- HTML Standard: processed with the in head rules
- canonical stayed inside the head
What this measurement does not show
One home page per host, on one day, read once by a scanner and once by a browser engine. That is the shape of the evidence and it bounds what can be claimed from it. A defect of this kind lives in a template, so a site whose home page is clean may well have an interior template that is not, and nothing here looked at interior pages. The reverse holds too.
The reads are not perfectly stable either, and the instability is worth stating rather than smoothing over. The measurement was run twice on 13 November 2026, a few hours apart, over the same 1,419 hostnames. 34 of the 35 affected pages appeared in both runs with the same offending element. One, magicalkenya.com, appeared in only one of the two: the site served different markup on the two reads, and on the read that counted it the element closing the head was a nav. A page assembled per request is a page that can be measured twice and answer twice.
Three gaps in the method, named because they are the ones that could move the number. The tag name pattern used here stops at an underscore, so an element named like a permitted one but with an underscore appended, meta_foo for instance, would have been read as meta and not counted. No such tag was observed, but none was searched for across the whole corpus either. Nothing inside a noscript, template, script or style block was evaluated, which is conservative and will undercount rather than overcount: a div inside a noscript block in the head does in fact close it under the standard's in head noscript rules, which apply when scripting is disabled, and that is the case for a crawler running no JavaScript. And no crawler other than a browser engine was tested. Whether GPTBot, ClaudeBot or PerplexityBot each run a fully conformant HTML parser is not something this measurement could see, and assuming they match Chromium is an assumption rather than a finding.
The honest summary is that a famous HTML defect turned up on 35 of 1,057 home pages and cost almost all of them nothing, which is not the conclusion this measurement was set up to find. That is worth saying plainly on a blog that sells measurement. The gap between what a page declares and what a machine receives is the thing worth auditing, and this is one of the narrower versions of it: much narrower than 141 of 382 home pages carrying no structured data in the raw HTML, and narrower than the 85,359 HTML comments that reached no extraction at all. Declaring something is not the same as delivering it, which is what prose parity measures on the text side and what AI visibility means when this site uses the phrase. For one hospital network on 13 November 2026, the canonical was declared and not delivered.
-
Head closed early35 of 1,057 pages an element the in head insertion mode does not permit, on one home page per host -
Nothing lost18 of the 35 the offending element sat after every declaration in the head -
Canonical moved1 page uhn.ca, confirmed with Chromium's parser over the same bytes -
Title moved1 page, still applied maxbone.com, document title resolved despite the element being in the body -
Interior pagesnot measured a template defect can differ between a home page and the pages below it -
Underscored tag namesnot searched the pattern used stops at an underscore, so meta_foo would read as meta -
Non-browser parsersnot tested whether each AI crawler parses exactly as Chromium does was not observable here
Lantad
Published .
There is a sentence in Google's documentation on canonicalization that reads like housekeeping and is actually a warning. The canonical link element is only accepted if it appears in the head section of the HTML, it says, so make sure at least the head section is valid HTML. The second clause is the one worth stopping on. It concedes that a head section can be invalid, that the invalidity can push a tag out of the head without anybody editing that tag, and that the tag then stops counting. The page carries a last updated date of 10 July 2026 and was read at source on 13 November 2026.
Common questions
Why is my canonical tag not working when it is in the head?
Check whether anything invalid appears in the head above it. Google's canonicalization documentation, last updated 10 July 2026, states that the canonical link element is only accepted if it appears in the head section of the HTML, and the HTML Standard ends the head at the first element the in head insertion mode does not permit, such as a div, a span, a hidden input or an unknown custom element. A canonical written after that point is parsed into the body even though the source looks correct. Of 1,057 corpus home pages Lantad read on 13 November 2026, 35 closed their head this way and one of them, uhn.ca, had its canonical parsed into the body.
Does an invalid head break structured data and robots meta tags too?
No, according to Google's own documentation. The robots meta tag page, last updated 24 March 2026, says Google Search does not enforce placement in the head and will respect the tag in the body section as well, and the introduction to structured data, last updated 10 December 2025, documents JSON-LD for both the head and body elements. Nine of the 35 affected pages had a JSON-LD block parsed outside the head and none had a robots meta tag out of place. The canonical link element was the only declaration in this measurement with a documented head-only requirement.
Which elements are allowed inside the head element?
Metadata content only: base, basefont, bgsound, link, meta, noscript, noframes, style, script, template and title, per the HTML Standard's in head insertion mode, with MDN describing the permitted content as one or more elements of metadata content of which exactly one is a title. Anything else, including a div, a span, an img, an input, a header, a nav and any custom or misspelled tag name, triggers the standard's Anything else rule: the head element is popped off the stack of open elements and parsing continues in the body.
How often did this cost a site something real?
Once in 1,057 pages, on the measurement Lantad ran on 13 November 2026. 35 pages closed their head early, 18 of those lost nothing because the offending element came after every declaration, and the 17 that orphaned declarations mostly orphaned stylesheets, preloads, icons and Open Graph properties, which continue to work in the body. 88 of the 172 orphaned declarations were asset or performance hints. Exactly one page, uhn.ca, moved a declaration that its publisher's documentation says is only honoured in the head.
See what AI can read on your site
Run a free scan and get a graded report of exactly what AI crawlers can and cannot read, with ranked fixes.