Skip to content

Best practices

HTML validation errors: which ones matter and which are noise

A validator can list 200 errors on a page that works. Learn the handful that change how browsers, screen readers and Google read the page, how to find them, and which ones you can safely ignore.

getReport teamUpdated 25 Sept 202611 min read

Run almost any real website through the W3C validator and you get a long list of errors. Most of them change nothing: the browser repairs the markup the same way every time and the page works. A few change the page itself, moving a canonical tag out of <head>, pulling text out of its section or breaking the link between a label and its field. This guide shows how to tell the two apart, fix the ones that matter and stop worrying about the rest. Expect an hour for a typical template.

Quick answer

  • Google does not rank pages on validity, and browsers repair most errors. Chasing zero errors is not the goal.
  • Fix the errors that change the document: a missing doctype, a late or missing charset, anything that ends <head> early, duplicate id values, unclosed or misnested elements that move content, broken list and table structure.
  • Ignore the noise: obsolete presentational attributes, type="text/javascript", trailing slashes on <br />, framework attributes such as v-if or x-data.
  • Validate the HTML your server sends and look at the repaired DOM in DevTools; for JavaScript-built pages, validate the rendered DOM too.
  • The SEO audit checks the viewport tag; the full report adds the doctype and charset checks.

Why HTML validation errors matter (some of them)

HTML has a precise error-recovery algorithm. When a browser meets a <div> inside a <p> or a stray </div>, it does not guess; it follows rules every modern browser shares. That is why invalid pages usually look fine, and why Google has said for years that valid markup is not a ranking factor.

The problem is what the repair produces. The browser builds a document tree that can differ from the one the template author imagined:

  • Head elements that land in the body. If <head> contains something that does not belong there (a <div>, an <img>, stray text), the parser closes the head at that point. Every <meta> and <link> after it is read as part of the body. Google's documentation on valid page metadata says it stops reading the head at the first invalid element, so a canonical, a robots meta or an og:image after that point can be ignored.
  • Content that moves. A block element inside a <p> closes the paragraph early. Text or elements placed directly inside a <table> but outside a cell are moved out and shown above the table. Headings and paragraphs can end up outside the section or article they were written in.
  • Relationships that break. Two elements with the same id mean <label for>, aria-labelledby, aria-describedby and #anchor links point at whichever comes first. The form field with the duplicate loses its label for screen reader users.
  • Rendering modes that change. Without a doctype, browsers switch to quirks mode, where box sizing, table font sizes and some line heights follow 1990s rules.

How getReport checks it

getReport checks the errors with the largest effect as separate findings rather than as one validator count. The SEO audit covers the viewport tag; the doctype and charset checks are in the Best practices module of the full report, one click from the tool.

The Best practices module of the example shop report: score 88, one failed finding and six warnings about caching, compression, images and HTTP/1.1, and a collapsed "Show 16 passed checks" link
Doctype and charset pass on this shop, so they sit in the collapsed list of passed checks rather than among the findings.

The doctype check reads the start of the HTML the server sent. If <!doctype html> is not the first thing there (a byte-order mark and whitespace are allowed), it asks the rendered page whether the browser found a doctype at all. Only a page with no doctype gets the warning. A legacy HTML 4 or XHTML doctype normally passes too, even though some of them still put the browser in quirks or limited-quirks mode; step 2 shows how to tell.

The charset check passes when a <meta charset> sits within the first 1,024 bytes of the document or the Content-Type header declares a charset. A meta tag after the first 1,024 bytes, or no declaration anywhere, is a warning.

A full validator count is catalogued as html-valid-errors-count, an information line with no weight, but it is not part of reports yet. Until it is, use the W3C validator as described below. The accessibility checker catches several structural errors from the accessibility side: axe-core flags lists with invalid children and duplicate ids referenced by ARIA.

Step by step

1. Validate the page and sort the list

Paste the page's address into the Nu HTML Checker from the W3C. It reports errors and warnings with line numbers in the source. Do not start at the top; sort the list with the triage table:

ErrorWhat it does to the pagePriority
No doctypeQuirks mode: layout differences between browsersFix now
Charset missing or lateGarbled accented letters, a second parseFix now
Element not allowed in <head>Ends the head; later meta and link tags may be ignoredFix now
Duplicate idLabels, ARIA references and anchors point at the wrong elementFix now
Stray end tag, unclosed elementContent moves out of its container; layout breaks after editsFix in the template
Non-<li> children in <ul>/<ol>Screen readers announce wrong item countsFix in the template
Content directly in <table>Moved above the tableFix in the template
<button> or <a> inside <a>Unpredictable clicks, confusing for assistive techFix when touched
Duplicate attributeThe second one is droppedFix when touched
More than one visible <main>Landmark navigation lands on the firstFix when touched
Obsolete attributes, type on scripts and styles, trailing slashesNothingIgnore
Unknown attributes from frameworksNothingIgnore

2. Fix the document-level errors

The first three lines of every page decide the rendering mode and the encoding. In your base template (header.php, theme.liquid, a layout component):

HTML
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Page title</title>
  <!-- then canonical, description, Open Graph, styles, scripts -->
</head>

Keep the lang value accurate, since screen readers pick their pronunciation from it (language attributes and screen readers), and never add user-scalable=no or maximum-scale=1 to the viewport tag (why pinch zoom must stay on).

<meta charset> goes first because the browser must know the encoding before it reads any text, including the title. If your server already sends Content-Type: text/html; charset=utf-8, the meta tag is a belt to those braces. A byte-order mark at the very start of a file overrides both declarations. The most common cause of garbled letters is simpler: a template or a database table saved in one encoding (Windows-1250, Latin-1) and declared as another. Save templates as UTF-8.

To see which mode the browser chose, type document.compatMode in the DevTools console: "CSS1Compat" is standards mode, "BackCompat" is quirks mode. The MDN page on quirks mode lists what changes.

3. Keep the head clean

Only <title>, <meta>, <link>, <script>, <style>, <base>, <noscript> and <template> belong in <head>. The usual intruders are tracking snippets: the <noscript><iframe> part of the Tag Manager snippet belongs right after <body>, not in the head, and a pixel's <img> fallback likewise. Plugins that print a <div> from a wp_head hook do the same damage. After a fix, the validator should no longer report "Element … not allowed as child of element head" and the Elements panel in DevTools should show your canonical and Open Graph tags inside <head>.

4. Fix duplicate ids

Search the validator output for "Duplicate ID". The usual sources are a component rendered twice (a search form in the header and again in the mobile menu, both with id="search"), copy-pasted blocks and sliders that clone slides. Make each id unique or remove ids nothing references:

HTML
<!-- Before: two fields, one label target -->
<label for="email">Email</label><input id="email" type="email">
<!-- … footer newsletter … -->
<label for="email">Email</label><input id="email" type="email">

<!-- After -->
<label for="contact-email">Email</label><input id="contact-email" type="email">
<label for="newsletter-email">Email</label><input id="newsletter-email" type="email">

This matters more for accessibility than for anything else: a screen reader announces the second field without its label.

5. Fix structure that moves content

"Stray end tag div" or "End tag div seen, but there were open elements" usually means one template opens a wrapper and another closes it, often across an if branch. Match every opening tag with its closing tag in the same file and the same branch:

HTML
<!-- Before: the wrapper only opens when there is a sidebar, but always closes -->
{% if sidebar %}<div class="with-sidebar">{% endif %}
  …
</div>

<!-- After -->
{% if sidebar %}<div class="with-sidebar">{% else %}<div>{% endif %}
  …
</div>

Lists take only <li> children (plus <script> and <template>); wrap extra markup inside the <li>. Tables need their content inside <td> or <th>. A <p> cannot contain <div>, <ul>, headings or another <p>; use a <div> as the outer element instead.

6. Leave the noise alone

These errors and warnings change nothing a visitor or a crawler can notice:

  • type="text/javascript" and type="text/css" (unnecessary, harmless).
  • Trailing slashes on void elements, <br /> and <img … />.
  • Obsolete presentational attributes such as border="0" or align="center": move them to CSS when you touch the template, not before.
  • Framework attributes such as v-if, @click, x-data or ng-…, and hyphenated custom elements. (data-… attributes are valid already.)
  • A <div> inside an <a>: valid since HTML5, as long as the link contains nothing interactive and its parent allows the <div>.

7. Validate in CI

For a static or server-rendered site, run the Nu checker on the built HTML so structural errors do not come back:

Shell
# Once: the checker as a dev dependency (needs Java 11+)
npm install --save-dev vnu-jar

# In CI, after the build: errors only, HTML files only
java -jar node_modules/vnu-jar/build/dist/vnu.jar --errors-only --skip-non-html dist/

It exits with a non-zero status when it finds errors. Start with the templates that matter most and add the rest once they are clean.

Platform notes

WordPress

The classic editor and older themes run content through wpautop, which wraps lines in <p>. A shortcode that outputs a <div> then ends up inside a paragraph, producing a stray </p> and an empty paragraph. Shortcode authors fix it by returning block markup without surrounding line breaks; site owners can put the shortcode in its own Shortcode block in the block editor. Page builders produce deeply nested but mostly valid markup; that is a DOM size problem, not a validation one. Duplicate ids are common with header and mobile-menu widgets that print the same form twice.

Shopify

Liquid conditionals that open a tag in one branch and close it outside the branch are the main source of misnesting. Sections and app blocks that each print their own <style> or <link> inside the body are allowed and not worth fixing.

Sites built with JavaScript

When a framework builds the page in the browser, the source the validator fetches may be an almost empty shell. Validate the rendered DOM as well: in the console, run copy('<!doctype html>\n' + document.documentElement.outerHTML) and paste the result into the validator's text input. outerHTML does not include the doctype, which is why the snippet adds it.

Verify

  • The validator lists no errors from the "fix now" rows of the triage table on your main templates.
  • The report's doctype and charset findings read "Doctype is present" and "Character encoding is declared early".
  • In DevTools, the Elements panel shows every <meta> and <link> inside <head>, and document.compatMode returns "CSS1Compat".

Common mistakes

  • Chasing zero errors. A day spent removing type attributes changes nothing. Fix the rows that change the document and stop.
  • Validating the source when JavaScript rewrites the page. The validator sees the shell, not what visitors get. Validate the rendered DOM too.
  • Fixing framework noise. Removing v-if or x-data to please the validator breaks the page. Leave them.
  • Ignoring duplicate ids. They look harmless in the list and quietly break labels and ARIA references. Search for them first.
  • Moving the charset below other tags. A plugin that inserts a long comment or script before <meta charset> can push it past the first 1,024 bytes. Keep it the first element in <head>.
Check your site before and after Check