Three rows in the Mega Analyzer Crawl Health card (on the SEO tab) ask whether the metadata you wrote survived parsing. <head> intact (metadata parsed inside head) checks that the canonical, description, robots, hreflang and Open Graph tags ended up in the head after the browser's parser was done with the page, not just in the source. Single title, description and canonical checks that each of those exists once. Canonical is absolute, https and unique checks that the one canonical is in a form Google will act on. The first row is the one that surprises people: the source looks fine, the tags are between <head> and </head> in view-source, and Google still never saw them.
What the check actually tests
Detector one: head integrity. The analyzer hands the fetched HTML to the browser's own parser (DOMParser) and then asks the parsed document, not the source text, where things landed. It queries document.body for eight kinds of element: link[rel="canonical"], meta[name="description"], meta[name="robots"], link[rel="alternate"][hreflang], any meta[property^="og:"], link[rel~="icon"], meta[name="viewport"], and <title> (ignoring <title> inside inline SVG). Every match is a head tag that the parser put in the body. Zero matches passes with <head> intact (metadata parsed inside head). One or more fails with <head> closed early: N head tag(s) landed in <body>, rated critical, which weighs double in the Crawl score. The detail lists up to six distinct kinds it found, in the shape link[rel=canonical], meta[name=description], link[rel=alternate].
To name the culprit, it goes back to the source text. It takes the literal span between <head> and </head>, strips comments and whole script, style, noscript, template and title blocks, and walks what is left. The first tag that is not title, meta, link, style, script, noscript, base, template or head becomes "after <div>" (or <img>, <svg>, <iframe>, <span>, whatever it was). The scan does not tell start tags from end tags, so a stray </div> that the parser ignored can be named as the culprit when something after it did the real damage. The first run of non-whitespace text becomes "after text" plus the first 30 characters. If the scan finds nothing, the row ends at "landed in <body>" with no suffix, and that silence is itself a clue; more on that in the fix section.
What does not trip it: a head that closes early after all eight kinds have already been emitted. The row counts metadata in the body, not the break itself, so <title>, description and canonical first, then a stray <div>, then a stylesheet and a twitter:card tag, passes. Stylesheets, scripts, meta[name="keywords"], Twitter card tags and the charset tag are not in the list and are never counted. Comments never close a head. Neither does a stray end tag such as </div>, because the parser ignores unknown end tags in head. And a <link> or <meta> that sits after </head> but before <body> is pushed back into the head by the parser's "after head" rules, so that common template sloppiness passes too.
Detector two: duplicate head tags. From the same parsed document it counts <title> (again excluding SVG titles), meta[name="description"], link[rel="canonical"] and meta[name="viewport"], anywhere in the document. Any count above one fails with Duplicate head tags: 2 <title>, 2 meta description, rated warn. Canonicals get a qualifier: 2 canonical (conflicting) when the hrefs differ, 2 canonical (identical) when a theme and a plugin both emitted the same URL. The pass wording is Single title, description and canonical. The attribute match is case sensitive, so <meta name="Description"> with a capital D is invisible to this counter even though Google reads it (its meta tags documentation says letter case is generally not important). Zero of something is not a duplicate; a missing description is reported by other rows, not this one.
Detector three: canonical validity. This runs only when at least one canonical exists; a page with none shows neither a pass nor a fail here. It reads the raw href attribute, not the resolved URL, and applies five rules. More than one distinct canonical href: "N conflicting canonicals". An href that does not begin with http:// or https://: "relative href" plus the value, which also catches protocol-relative //example.com/.... An http:// href on an https:// page: "points to http://". A hostname that differs from the page's hostname after stripping a leading www. from both: "points to another host". A # anywhere, or ?utm_ / &utm_: "carries a fragment or utm parameter". Any hit fails with Canonical invalid: and the list, rated warn, with the first 120 characters of the value in the detail. Clean passes with Canonical is absolute, https and unique. A canonical to a different path on the same host passes; so does one with a query string that is not a utm parameter, and so does one that swaps www for the apex.
One correction to the row's own detail text: it says Google requires an absolute URL. Google's wording is softer. Relative paths "are supported by Google" but "can cause problems in the long run" and are not recommended. Treat the row as a recommendation with a specific failure mode behind it, which is the testing site Google itself gives as the example.
Why it matters
The parser rule is short and unforgiving. In the HTML standard's "in head" insertion mode, tokens for title, meta, link, style, script, noscript, base and template (plus the obsolete basefont, bgsound and noframes) are handled in place, whitespace and comments are inserted, a repeated <html> or <head> start tag leaves the head alone, unknown end tags are ignored, and everything else hits the entry that reads: "Pop the current node (which will be the head element) off the stack of open elements. Switch the insertion mode to 'after head'. Reprocess the token." A <div>, an <img>, an <svg>, an <iframe>, a <span> and a run of visible text all land in that entry. The token is then reprocessed in "after head", where anything that is not a head element inserts a <body> and switches to "in body". From that point every <link> and <meta> in your source is a body element, because the "in body" rules insert them at the current position rather than moving them back.
Google says it follows the same rule. Its page on valid metadata lists exactly the elements a head may contain, names iframe and img as the common offenders, and states: "Once Google detects one of these invalid elements, it assumes the end of the <head> element and stops reading any further elements in the <head> element." Its canonicalization guide adds that "The rel="canonical" link element is only accepted if it appears in the <head> section of the HTML." Its meta tags reference says "the head section must be valid HTML and in case of attributes, all parent tags closed accordingly." So a canonical after the break is not a weaker canonical; it is no canonical at all, and the same goes for the description, the robots directive and the hreflang cluster.
The failure is quiet because Google has fallbacks for almost everything it loses. Its title link documentation lists the sources it draws on when generating a result title: "Content in <title> elements", the "Main visual title shown on the page", heading elements, "Content in og:title meta tags", and more. Lose the <title> to the body and the result still has a headline, just not yours. Lose the description and the snippet comes from page content, which its snippet documentation says is the default anyway; the meta description is used only when Google decides it describes the page better. Lose the canonical and Google "will identify which version of the URL is objectively the best version to show to users in Search," which is its wording for choosing one itself. Nothing 404s, nothing errors in the browser, and the Search Console symptom, if you notice it at all, is the Page indexing status "Duplicate, Google chose different canonical than user" on pages whose source plainly contains a self-canonical.
The mechanism is almost always a theme or plugin, and it is the same on every page at once. A cookie consent script that prints its placeholder <div> in the header include. A tracking pixel installed "at the very top of the page" that arrives as an <img>. An SVG icon sprite included before </head> so it is available everywhere. A <noscript> fallback wrapping an <img>. Text from a PHP include saved with a byte-order mark, or an that a page builder emitted between two tags. Each one is a single line in a shared template, and it ends the head on every page that uses the template.
Duplicates come from the same place. The theme emits a title and description, then an SEO plugin emits its own, and two of each ship. The analyzer's advice is to keep exactly one because a parser or search engine that sees two has to choose, and the row's detail says the choice usually lands on the first one, which is often the theme's generic version. Google's canonical guidance does not say what it does with two link elements that disagree; its closest rule is "Don't specify different URLs as canonical for the same page using different canonicalization techniques," and two canonical tags with different hrefs send that same contradiction through one technique. The page fails both this row and the validity row when it happens.
How to fix it
Step 1: find the break. Read the row's suffix. "after <div>" or "after <img>" points at the first tag in the literal head that cannot live there; open view-source and search for it. "after text" means a character run, so look for a stray ;, a --> that closed nothing, a leftover ]]> or a literal . When the row has no suffix, the parser saw the head end but the source scan did not find a visible offender, and there are three usual reasons: a <noscript> in the head that wraps an <img> or <iframe> (the scan skips noscript blocks), an invisible character such as a byte-order mark from an included file or a raw non-breaking space (the scan treats those as whitespace), or a source with no literal </head> at all. Paste the URL into the Head Integrity Audit; it reports the break with a line number, lists each tag that fell into the body, and separates the noscript case into its own warn row.
Step 2: move the offender below <body>. This is what a wrapper injected by a plugin does to a head:
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekly rates and availability</title>
<div id="consent-placeholder"></div>
<link rel="canonical" href="https://example.com/rates/">
<meta name="description" content="Nightly and weekly rates, updated every morning.">
<link rel="alternate" hreflang="es" href="https://example.com/es/tarifas/">
</head>
The parser stops the head at the <div>, creates a <body>, and the canonical, description and hreflang become the first three children of that body. The row reads <head> closed early: 3 head tag(s) landed in <body> after <div>. The fix is to relocate the one line, not the three:
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekly rates and availability</title>
<link rel="canonical" href="https://example.com/rates/">
<meta name="description" content="Nightly and weekly rates, updated every morning.">
<link rel="alternate" hreflang="es" href="https://example.com/es/tarifas/">
</head>
<body>
<div id="consent-placeholder"></div>
In WordPress this usually means a plugin is printing markup on the wp_head action when it belongs on wp_body_open, which fires "after the opening body tag". Check the plugin's settings for a placement option before patching it; many have one. In a static generator, the offender is in a header partial, and the move is the same edit in one file. If the tag is text rather than an element, delete it, and if it is a byte-order mark, resave the include as UTF-8 without BOM.
Step 3: keep exactly one of each. When the duplicate row names 2 <title> or 2 meta description, find both emitters. On WordPress, a theme that hardcodes <title> while an SEO plugin also outputs one is the classic pair; the theme should declare title-tag support and let one owner render it. On a static site, a layout that prints its own tags and a page that prints them again from its front matter does the same. Whichever you keep, the head skeleton you want on every template variant is one line of each:
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekly rates and availability | Example Motel</title>
<meta name="description" content="Nightly and weekly rates, updated every morning.">
<link rel="canonical" href="https://example.com/rates/">
<meta property="og:title" content="Weekly rates and availability">
<link rel="stylesheet" href="/css/site.css">
</head>
Step 4: make the canonical absolute, https, and self-referencing. Replace href="/rates/" and href="//example.com/rates/" with the full https:// URL. Build it from one site URL setting plus the page path, so the scheme and host come from a single source and a later domain move changes one value. Strip fragments and tracking parameters; a canonical that carries ?utm_source= is telling Google to index the tracked variant. If the validity row says "points to another host" and the host is not a leftover from staging or an old domain, read the canonical target post before deciding, because that case can be deliberate.
Step 5: gate it. All three rows reduce to a check you can run against built output before deploying. This one walks the literal head the way the analyzer does, treats anything inside <noscript> as markup (which is how a scripting-off parser reads it), counts the three tags, and checks the canonical's form:
// scripts/check-head.mjs (run before every deploy; exit 1 blocks it)
import fs from 'node:fs';
import path from 'node:path';
const HEAD_OK = new Set(['html', 'head', 'title', 'meta', 'link', 'style', 'script', 'noscript', 'base', 'template', 'noframes']);
const problems = [];
function checkHead(file, html) {
const m = html.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i);
if (!m) { problems.push(file + ': no literal <head>...</head> pair'); return; }
const raw = m[1].replace(/<!--[\s\S]*?-->/g, '').replace(/<(script|style|template|title|noframes)\b[\s\S]*?<\/\1>/gi, '');
const re = /<\/?([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*>|([^<]+)/g;
let t;
while ((t = re.exec(raw))) {
if (t[1]) {
const tag = t[1].toLowerCase();
if (!HEAD_OK.has(tag)) { problems.push(file + ': <' + tag + '> inside <head> ends the head'); return; }
} else if (/[^\t\n\f\r ]/.test(t[2])) {
problems.push(file + ': text inside <head> ends the head: ' + JSON.stringify(t[2].trim().slice(0, 30))); return;
}
}
for (const [label, tagRe] of [['<title>', /<title\b/gi], ['meta description', /<meta\b[^>]*\bname=["']description["']/gi], ['canonical', /<link\b[^>]*\brel=["']canonical["']/gi]]) {
const n = (html.match(tagRe) || []).length;
if (n !== 1) problems.push(file + ': ' + n + ' ' + label + ' tag(s), want exactly 1');
}
const tag = (html.match(/<link\b[^>]*\brel=["']canonical["'][^>]*>/i) || [])[0] || '';
const href = (tag.match(/\bhref=["']([^"']+)["']/i) || [])[1] || '';
if (href && (!/^https:\/\//i.test(href) || /#|[?&]utm_/i.test(href))) problems.push(file + ': canonical is not a clean absolute https URL: ' + href);
}
function walk(dir) {
for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, f.name);
if (f.isDirectory()) walk(p);
else if (f.name.endsWith('.html')) checkHead(p, fs.readFileSync(p, 'utf8'));
}
}
walk('_site');
if (problems.length) { console.error(problems.join('\n')); process.exit(1); }
console.log('head integrity: clean');
The text test is stricter than the analyzer's on purpose: the HTML parser treats only tab, line feed, form feed, carriage return and space as whitespace in the head, so a non-breaking space or a byte-order mark fails here even though the analyzer's source scan would skip it.
Step 6: verify at site scale. Re-run the Mega Analyzer and confirm all three rows pass. Then run the sitemap through the Duplicate Title + Meta Description Audit, which fetches up to 500 sitemap URLs, reads the first title and description on each, and reports every value that is missing or shared across pages, so a title hardcoded in the template shows up as one value used on dozens of URLs rather than as one row on one page. It reads the first tag only, so doubled tags on a single page stay the Mega Analyzer's job. Run the sitemap (or a pasted list, up to 100 URLs) through the Canonical Cluster Map as well, which resolves each canonical and groups pages by target, then flags canonicals that point outside the set, at a noindex page, across http and https, or in a circle. Because it resolves relative hrefs before grouping, a relative canonical looks normal there; only the validity row catches the form. If a page ends up short a title, description or canonical after you removed the wrong duplicate, the Code-Diff Patch Generator emits each missing tag as a diff-style before-and-after snippet for the head.
When to leave it alone
A head that closes early after everything Google needs is the one case the documentation itself allows. Google's wording is "We strongly recommend that you don't use these invalid elements in the <head> element, but if you must, place these invalid elements after the ones you want Google to see." If a vendor snippet insists on head placement and you cannot move it, order it last, after the title, canonical, description, robots, viewport, icon, hreflang and Open Graph tags. The analyzer passes that layout; the Head Integrity Audit reports it as a warn with no metadata following, which is the honest description.
The <noscript> pixel is a judgment call. With scripting on, a browser parses a head <noscript> as raw text and the head stays open, which is why DevTools shows nothing wrong. With scripting off, the standard's "in head noscript" rules let only link, meta, style, noframes, basefont and bgsound appear inside it, and anything else pops both the noscript and the head; DOMParser runs with scripting disabled, so the analyzer takes that path. Google's documentation lists noscript as valid in the head and says nothing about what it does with an <img> inside one. Moving the fallback below <body> costs nothing and removes the question, but if you cannot, put it last and treat the row as informational.
Two identical canonicals are a warn you can schedule rather than rush. They agree, so nothing is lost; fix the double emitter the next time you touch the template. Conflicting canonicals are not in that category. A canonical that points to another host for a deliberate syndication copy will fail the validity row every time; keep it, and write down why, as the canonical target post describes. And a canonical to a different path on the same host, such as a parameterized URL pointing at its clean form, is exactly what the tag is for and does not trip any of these rows.
Fact-check notes and sources
- Source: https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhead establishes the "in head" insertion mode:
base,basefont,bgsound,link,meta,title,noframes,style,script,templateandnoscriptare handled in place (noscriptas raw text when scripting is enabled, through the "in head noscript" mode when it is disabled), the whitespace it inserts is only U+0009, U+000A, U+000C, U+000D and U+0020, "Any other end tag" is a parse error that is ignored, and "Anything else" means "Pop the current node (which will be the head element) off the stack of open elements. Switch the insertion mode to 'after head'. Reprocess the token." The same page's "in head noscript" mode allows onlybasefont,bgsound,link,meta,noframesandstyleinside a headnoscript, and its "after head" mode pushes a straybase,basefont,bgsound,link,meta,noframes,script,style,templateortitleback into the head while "Anything else" inserts abody. - Source: https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-domparser-parsefromstring establishes that for
DOMParser.parseFromStringwithtext/html, "Since document does not have a browsing context, scripting is disabled." - Source: https://developers.google.com/search/docs/crawling-indexing/valid-page-metadata establishes the list of elements a head may contain (
title,meta,link,script,style,base,noscript,template), that "Common elements that appear in the<head>element, rendering it invalid are:iframe,img," that "Once Google detects one of these invalid elements, it assumes the end of the<head>element and stops reading any further elements in the<head>element," and the advice to "place these invalid elements after the ones you want Google to see." - Source: https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls establishes that "The
rel="canonical"link element is only accepted if it appears in the<head>section of the HTML," that relative paths "are supported by Google" but "can cause problems in the long run" and are not recommended, that "Google prefers HTTPS pages over equivalent HTTP pages as canonical," that "if you don't specify a canonical URL, Google will identify which version of the URL is objectively the best version to show to users in Search," and the rule "Don't specify different URLs as canonical for the same page using different canonicalization techniques." - Source: https://developers.google.com/search/docs/crawling-indexing/special-tags establishes that "the
headsection must be valid HTML and in case of attributes, all parent tags closed accordingly," and that "With the exception ofgoogle-site-verification, letter case is generally not important inmetatags." - Source: https://developers.google.com/search/docs/appearance/title-link establishes the guideline to "Make sure every page on your site has a title specified in the
<title>element" and the list of sources Google uses to generate title links, including<title>content, the main visual title, heading elements andog:title. - Source: https://developers.google.com/search/docs/appearance/snippet establishes that "Snippets are automatically created from page content" and that "Google sometimes uses the meta description HTML element if it might give users a more accurate description of the page than content taken directly from the page."
- Source: https://support.google.com/webmasters/answer/7440203 establishes the Page indexing status "Duplicate, Google chose different canonical than user": "This page is marked as canonical for a set of pages, but Google thinks another URL makes a better canonical."
- Source: https://developer.wordpress.org/reference/hooks/wp_body_open/ establishes that the
wp_body_openaction is "Triggered after the opening body tag" and was introduced in WordPress 5.2.0; https://developer.wordpress.org/reference/functions/add_theme_support/ establishes thetitle-tagfeature (WordPress 4.1.0), which "enables plugins and themes to manage the document title tag." - The list of eight selectors, the allow list of the source scan, the SVG title exclusion, the case sensitive attribute match, the five canonical rules and the www tolerance come from the Mega Analyzer source at https://jwatte.com/tools/mega-analyzer/ (view source; search for
gap_head_integrity,gap_dup_headandgap_canonical_valid).
Related reading
- Which URL is the real one? Off-domain canonicals and sitemaps with two homepages
- Why a Duplicate Title and Meta Description Audit Exists
- Main and nav landmarks, one title, unique IDs: the skeleton screen readers rely on
- Open Graph Card Preview: What LinkedIn, Slack, and Discord Actually See
One shared header partial with one bad line in it is how a defect on one site becomes a defect on every site that shares the template. The $100 Network makes the case for auditing the template once and gating the build, rather than checking pages one at a time, for exactly this reason.