How to check for invalid elements in the <head>

Short answer

Only eight elements are allowed in the <head>. If anything else turns up, such as an <img>, <iframe> or <div>, the browser decides the head has ended and treats every tag after it as part of the body. Google does the same: it ignores the elements that come after the invalid one, so a canonical, hreflang or robots tag below that point can be missed.

<title><meta><link><script><style><base><noscript><template>

Why one stray tag matters

Browsers don't throw errors on bad HTML. They repair it. When the HTML parser meets something that can't live in the head, it closes the head right there and starts the body, even though your </head> is further down.

A script that ends up in the body still runs, so that does no harm. SEO tags are different. Everything after the stray element is now in the body, and Google stops reading the head at that point, so those tags can be missed. Google's own guidance on valid page metadata says it treats an invalid element as the end of the head.

What your server sends view-source
<head>
  <meta charset="utf-8">
  <title>Blue Widgets</title>
  <img src="/pixel.gif">
  <link rel="canonical" …>
  <meta name="robots" …>
</head>
<body> …
What the browser builds DevTools Elements
<head>
  <meta charset="utf-8">
  <title>Blue Widgets</title>
</head> ← closed early
<body>
  <img src="/pixel.gif">
  <link rel="canonical" …> ← now in body
  <meta name="robots" …> ← now in body

Same HTML, two views. This is also why the problem is easy to miss: DevTools shows you the repaired version on the right, not what you shipped.

What usually causes it

It's rarely a developer writing <img> in the head by hand. It's normally something pasted in or injected:

Tag manager snippets The Google Tag Manager <noscript><iframe> belongs straight after <body>, but it often gets pasted into the head with the script part. With JavaScript on, browsers read it as text. A parser with JavaScript off ends the head at the <iframe>, and some SEO crawlers flag it, so move it anyway.
Tracking pixels Ad and analytics pixels that fall back to an <img>, added through a theme setting or a "header scripts" box.
Apps and plugins Shopify apps editing theme.liquid, WordPress plugins hooking into wp_head and printing a <div> or a stray <p>.
Stray text A loose character such as } or a word left behind by a template error. Text that isn't whitespace ends the head too.
SVG sprites and widgets Icon sprites, cookie banners or chat widgets output at the top of the template instead of in the body.

How to check by hand

Three ways. Methods 1 and 3 read the HTML your server sends. Method 2 reads the page the browser built, which is where the damage shows.

1Read the page source 2 minutes

  1. Open the page and press Ctrl+U (⌘+⌥+U on a Mac) to see the source.
  2. Press Ctrl+F and search for </head> (or <body if there isn't one). Everything above it is your head.
  3. Read down from <head>. Any tag that isn't one of the eight above, or any loose text, is the problem. Note which SEO tags come after it.

Fine on a short head. Tiring on a 400-line one full of inline scripts.

2Look for SEO tags that ended up in the body 1 minute

This flips the problem round: instead of finding the stray tag, look for the damage. Open DevTools, go to the Console and paste:

// SEO tags that the browser moved out of the head
document.querySelectorAll(
  'body link[rel~=canonical i], body link[hreflang], body meta[name=robots i], body meta[name=googlebot i], body meta[name=description i], body meta[name=viewport i], body meta[charset], body base, body title:not(svg title)'
).forEach(el => console.log(el.outerHTML, '\n  after:', el.previousElementSibling?.outerHTML.slice(0, 120)));

Nothing printed means nothing was pushed out. Anything printed is a tag search engines may be ignoring. The element shown after "after:" is usually the stray one, or close to it.

3Scan the server HTML with a snippet 1 minute

This fetches the page's HTML again and lists every tag in the head that isn't allowed:

// Lists elements in the server HTML head that don't belong there
const html = await (await fetch(location.href, { cache: 'no-store', headers: { Accept: 'text/html' } })).text();
// Drop comments and the contents of script-like elements first, then keep everything before the body.
const head = html
  .replace(/<!--[\s\S]*?-->/g, '')
  .replace(/<(script|style|noscript|template|title|textarea)\b[\s\S]*?<\/\1\s*>/gi, '')
  .split(/<\/head\s*>|<body[\s>]/i)[0];
const ok = /^(html|head|meta|link|base|script|style|noscript|template|title)$/i;
const bad = [...head.matchAll(/<([a-z][\w-]*)/gi)].map(m => m[1].toLowerCase()).filter(t => !ok.test(t));
console.log(bad.length ? 'Not allowed in <head>: ' + [...new Set(bad)].join(', ') : 'Head looks clean');

It's a rough pattern match, not a real parser. It won't catch loose text or anything inside <noscript>, and some sites send different HTML to Googlebot than to your browser.

What about elements added by JavaScript?

A script can insert a <div> into the head after the page loads. Inserting it that way doesn't re-run the parser, so nothing gets moved and it stays in the head. It's still worth cleaning up, but the server HTML is the part that pushes your tags out, so start there. The exception is document.write, which old pixel tags still use: it writes into the parser while the page loads, and can end the head just like server HTML.

So if methods 2 and 3 both come back clean but a tool still reports an <iframe> or <img> in the head, a script almost certainly added it. To see what's there, paste:

// Elements in the live head that don't belong there (including ones added by scripts)
const ok = /^(meta|link|base|script|style|noscript|template|title)$/i;
[...document.head.children].filter(el => !ok.test(el.tagName))
  .forEach(el => console.log(el.outerHTML.slice(0, 200)));

The src usually tells you which tag or vendor added it. With the server HTML clean and nothing printed by method 2, it's low priority: tidy it up through the tag manager or app settings when you can.

SEO tags in the body A title, meta description, canonical, hreflang, robots, viewport, charset or base tag that sits in the <body>, whether a stray element pushed it there or a template or plugin printed it in the wrong place. Search engines read these from the head, so they may be ignored. Method 2 above lists them.
Two <head> sections A template or plugin that outputs a whole second <head>. Browsers ignore the second <head> tag itself. If it comes before <body>, its tags are merged into the first head, which can leave two titles or two canonicals. If it's inside the body, for example a widget that prints a whole page, its tags land in the body and may be ignored. Either way, which tags count becomes hard to predict. Press Ctrl+U and search for <head: there should be one match (ignore <header).

How to fix it

  1. Move the stray element into the body. For tag manager and pixel fallbacks, put the <noscript> part straight after the opening <body> tag.
  2. If you can't move it (a locked app or plugin), move your title, meta description, canonical, robots and hreflang tags above it, as near the top of the head as you can.
  3. Check again with method 2. The list should come back empty.

On Shopify, WordPress and Next.js

  • Shopify: Go to Online Store > Themes, open Edit code and read the head in theme.liquid. Apps often add code there, so if the stray element isn't in the file, check recently installed apps.
  • WordPress: Anything hooked to wp_head can print into the head. Disable plugins one at a time on a staging site until the stray element disappears, then check the theme's header.php.
  • Next.js: Since version 15.2, metadata from generateMetadata can be streamed and added to the <body>, so a check can report it outside the head. Next.js says Googlebot reads it correctly. To keep it in the head for every visitor, set htmlLimitedBots: /.*/ in next.config.js, though that can slow responses.

How crawlers report this

If a crawl sent you here, these are the labels to look for. Our guide to Screaming Frog and Sitebulb issues covers other common ones.

Screaming FrogSitebulbWhat it means
Validation: Invalid HTML Elements In <head><head> contains invalid HTML elementsSomething that isn't allowed in the head, such as an image or iframe, ended it early. Tags after it may be ignored.
Validation: Multiple <head> TagsNo direct matchThe HTML has more than one head, usually from a stray template include.
Page Titles: Outside <head>Title tag outside of headThe title sits in the body, so it may not be used.
Meta Description: Outside <head>Meta description outside of headThe description sits in the body, so it may not be used.
Canonicals: Outside <head>Canonical outside of headThe canonical sits in the body, where Google ignores it.
Directives: Outside <head>Meta robots found outside of <head>A robots tag sits in the body. Find out why before you remove it, as a stray noindex can still do damage.

Doing this on every page?

SEOSnip runs this check for you

Open the extension on any page and it reads the server HTML and the rendered page side by side. If something breaks the head, you get:

  • The first stray element and its line in the server HTML
  • Which SEO tags it pushed out of the head
  • SEO tags that sit in the body for any other reason, with where each one is
  • More than one <head> in the server HTML, with the line of each
  • A ticket with that evidence attached, ready for a developer
  • A "Check if fixed" button that re-runs the check after the change ships