1. Structure
  2. HTML
  3. Content models and validity

Lesson 04 · Principles

Content models and validity

Every HTML element has rules about what may go inside it and where it may appear. Together those rules work like a type system, and a document that follows them is valid. Browsers never check them. They repair whatever arrives, in one defined way, and show the result. This lesson teaches the rules, shows what the parser does when they are broken, and puts the one tool that does check them, the validator, into your daily routine.

Learning objectives

  • Tell well-formed markup from valid markup, and explain why HTML needs both words
  • Describe how HTML's rules were written as a DTD, and how the living standard writes them now
  • Read an element's entry in the HTML standard: its categories, its contexts and its content model
  • Predict what the parser builds from the classic content-model violations, and check the prediction in DevTools
  • Run the Nu HTML Checker and html-validate, read their messages, and fit them into a code, validate, preview cycle

Well-formed and valid

Two questions can be asked of any markup. The first is about its surface: do the tags balance, are the attribute values quoted, does anything overlap? Markup that passes is well-formed. The second is about its meaning: is every element one the language defines, in a place the language allows, with attributes and values the language accepts? Markup that passes both tests is valid.

The two are easy to tell apart with a line of nonsense:

<Fizz>Buzz<baz bar="true"> Wat</baz> <blurp /></Fizz>

Every tag is closed, every element sits fully inside its parent, and the one attribute value is quoted. An XML parser would accept it without complaint. It is still not HTML: there is no Fizz, baz or blurp element in the language, so the line is well-formed and invalid.

In HTML the line between the two is blurrier than in XML, because HTML's surface grammar is forgiving on purpose. The end tags of p and li may be left out, so may the html, head and body tags altogether, and an attribute value without spaces or quotes in it needs no quotes. Markup that would be malformed in XML can be valid HTML. The opposite happens too: <p><div>hello</div></p> balances perfectly and is invalid, because a paragraph may only contain phrasing content. So this series uses well-formed loosely, for the tag-level grammar, and puts the weight on valid. The HTML standard itself prefers the word conforming; the two mean the same thing here.

Checking a document against the rules is called validation, and a program that does it is a validator. In HTML's case a validator is the only reader that ever enforces the rules. Lesson 01 called this the asymmetric contract: authors must write conforming documents, and browsers must accept anything. The rest of this lesson is about the strict half of that contract, the half that runs only when you run it.

From a DTD to a living standard

HTML has written its rules down in two very different ways.

The DTD era

From HTML 2.0 to HTML 4.01, HTML was formally an application of SGML, and its grammar lived in a Document Type Definition. The DTD declares each element, its attributes and what it may contain. If the DTD is a class, each document is an instance, and validation asks whether the instance conforms. Four declarations from the HTML 4.01 Strict DTD show how formal HTML once was:

<!ELEMENT P - O (%inline;)* -- paragraph --> <!ELEMENT UL - - (LI)+ -- unordered list --> <!ELEMENT A - - (%inline;)* -(A) -- anchor --> <!ELEMENT BUTTON - - (%flow;)* -(A|%formctrl;|FORM|FIELDSET) -- push button -->

After the element's name come two characters for its tags: - - means both tags are required, and - O means the end tag may be omitted, so an unclosed paragraph was legal long before HTML5. Then comes the content model. %inline; is a macro that expands to the inline elements plus text, so a paragraph holds zero or more inline things; a UL holds one or more LIs and nothing else. The last part of the A declaration, -(A), is an exclusion: no A may appear anywhere inside an A, however deep. The BUTTON line excludes links, form controls, forms and fieldsets in the same way. Most of the rules this lesson teaches were already there, as grammar.

The trouble was that only validators read the DTD. Browsers never parsed HTML as SGML. The standard's own account is blunt: "the only user agents to strictly handle HTML as an SGML application have historically been validators," and the confusion, "with validators claiming documents to have one representation while widely deployed web browsers interoperably implemented a different representation," wasted "decades of productivity."

The living standard

HTML5 cut the tie to SGML. The living standard says that while HTML's syntax "bears a close resemblance to SGML and XML, it is a separate language with its own parsing rules." There is no DTD for HTML today. The short <!doctype html> at the top of a page names no DTD at all; the standard keeps it only "for legacy reasons," to put the browser in standards mode, as lesson 02 explains.

The rules now live in prose. Every element's entry opens with a block stating its categories, where it may be used and what it may contain, and the parsing chapter states what a browser does with every possible input. Prose can say things a DTD cannot, such as that a button may have no descendant with a tabindex, or that headings may go down only one level at a time. Validators implement those rules in code.

Content categories: a type system

Rather than list every allowed parent and child pair, the standard groups elements into content categories and states each content model in terms of them. A paragraph's content model is "phrasing content." A section's is "flow content." An element can belong to several categories at once.

Read it as a type system. The categories are types, and a content model is a signature that says which types an element accepts as children. Phrasing content is a subtype of flow content: every phrasing element is also flow, so it can go anywhere flow is allowed, but not the reverse. Putting a div inside a p is a type error, exactly like passing a Shape where a Circle is expected. No compiler stops you; the browser runs the program anyway, after repairing it.

The standard defines seven main categories:

The seven main content categories
CategoryWhat it isExamples
MetadataSets up the presentation or behavior of the rest of the document, or its relationship to other documentstitle, base, link, meta, style, script, noscript, template
FlowMost elements used in the body: the default category for contentp, div, ul, table, form, main, and every element in the categories below
SectioningDefines the scope of header and footer elementsarticle, aside, nav, section
HeadingDefines the heading of a sectionh1 to h6, hgroup
PhrasingThe text of the document and the elements that mark it up within a paragraphtext, a, em, strong, span, code, img, button, input
EmbeddedImports another resource, or content from another vocabularyimg, picture, video, audio, iframe, canvas, svg, math
InteractiveContent specifically intended for user interactiona with href, button, details, label, select, textarea, input unless hidden, audio and video with controls
The content categories as overlapping setsA large rectangle labeled Flow holds most elements. Inside it are smaller sets: Sectioning (article, aside, nav, section), Heading (h1 to h6, hgroup), and a large Phrasing set (text, em, strong, span, code). Inside Phrasing is Embedded (img, video, audio, iframe, svg, canvas). A dashed Interactive set overlaps the right edge of Phrasing: a with href, button, input, label and select are phrasing and interactive; details is interactive but only flow. A dashed Metadata set overlaps the left edge of Flow: title, style and base are only metadata; link, meta, script, noscript and template are both. Flow-only elements such as p, div, ul, table and form sit at the bottom of Flow. Below the diagram: li, tr, td, option, dt and dd belong to no category and may appear only inside their own parents.FlowSectioningarticle aside nav sectionHeadingh1…h6 hgroupPhrasingtext em strong span codeEmbeddedimg video audioiframe svg canvaspicture mathInteractivea[href] buttoninput labelselect textareadetailsflow only: p div ul ol dl table form main blockquoteMetadatatitlestylebaselinkmetascriptnoscripttemplateNo category: li, tr, td, option, dt, dd. Each may appear only inside its own parent.
Categories are overlapping sets, not a tree. An element's content model names a set; membership decides what may nest where. (Simplified: some memberships depend on attributes, and link and meta count as phrasing only in some uses.)

Two lessons from the diagram matter more than the rest. First, the gap between flow and phrasing is where most errors happen. A p, div, ul, table or heading is flow content and not phrasing, so none of them may go inside a paragraph, a span, an em or a button. Second, some elements belong to no category at all. A li, a tr, a td, an option, a dt and a dd exist only inside one kind of parent, and they are invalid anywhere else.

Palpable and script-supporting content

Two more categories cut across the seven. Palpable content is content a person can perceive: non-empty text, an image, a control. As "a general rule," an element that accepts flow or phrasing content should contain some, but the standard adds that this "is not a hard requirement," since an empty element may be a placeholder for a script to fill. The Nu HTML Checker says nothing about an empty <p></p> and only warns, "Empty heading," about an empty <h2></h2>.

Script-supporting elements are script and template. They render nothing, so the standard lets them appear in places that otherwise have strict models: a ul's content model is "zero or more li and script-supporting elements," so a template for a list item can sit inside the list it will be cloned into.

Reading an element's entry

You do not need to memorize the categories; you need to read an element's entry. Every element in the standard starts with the same block. Here is the one for p, slightly shortened:

Categories: Flow content. Palpable content. Contexts in which this element can be used: Where flow content is expected. As a child of an hgroup element. Content model: Phrasing content. Tag omission in text/html: A p element's end tag can be omitted if the p element is immediately followed by an address, article, aside, blockquote, details, dialog, div, ... or ul element, or if there is no more content in the parent element and the parent element is an HTML element that is not an a, audio, del, ins, map, noscript, or video element, or an autonomous custom element. Content attributes: Global attributes DOM interface: HTMLParagraphElement

Read the first three fields as a sentence about types. Categories says what type the element is: p is flow, not phrasing. Contexts says where it may appear: wherever flow content is expected. Content model says what type it accepts: phrasing content only. Put those together and the rule "no paragraph inside a paragraph" falls out without being stated anywhere: the inner p is flow, the outer one accepts phrasing, and flow is not phrasing.

Tag omission is the parser's side of the same rule. It lists the elements whose start tag ends an open paragraph, all of them flow content that is not phrasing, so leaving out </p> before them is legal. Note the exceptions at the end. Most of them, a, audio, del, ins, map and video, are the transparent elements of the next section.

Three more entries, read the same way. ul is flow, used where flow is expected, and its content model is "zero or more li and script-supporting elements," so text or any other element directly inside it is invalid. li's entry inverts the pattern. Its categories are "None," and its contexts are "inside ol elements, inside ul elements, inside menu elements." Its own content model is generous: flow content, so a list item may hold paragraphs, headings and whole nested lists.

button is flow, phrasing, interactive and palpable, plus several form categories, so it may sit inside a paragraph. Its content model is "phrasing content, but there must be no interactive content descendant and no descendant with the tabindex attribute specified." So a button's label may be marked up with em or an img, but not with a link, another button, or a div.

The entry for a adds something new. Its categories are flow, phrasing, palpable and, only "if the element has an href attribute," interactive. Its content model is "transparent, but there must be no interactive content descendant, a element descendant, or descendant with the tabindex attribute specified." Transparent needs a section of its own.

Transparent content models

A transparent element takes its content model from its parent. The standard's definition: the elements allowed in the transparent part are "the same elements as required in the part of the content model of the parent of the transparent element in which the transparent element finds itself." Put plainly, the content must be valid as if the transparent element were not there, apart from any extra rule of its own.

<!-- Inside a section, which accepts flow content: a block link is valid --> <section> <a href="/notes/"> <h2>Course notes</h2> <p>Every lesson, with its demos.</p> </a> </section> <!-- Inside a heading, which accepts phrasing content: the same div is invalid --> <h2><a href="/notes/"><div>Course notes</div></a></h2>

The Nu HTML Checker reports the second case as "Element div not allowed as child of element a in this context," and adds a note worth reading: "The a element has a transparent content model; its allowed content is inherited from its parent element." The a is not the problem. The heading around it is.

The transparent elements are a, ins, del, map, object, canvas and slot, plus audio and video, whose fallback content (what shows when the media cannot play) is transparent after any source and track children. Each exists to wrap content without changing what kind of content it is. ins and del mark an edit, and an edit may cover a few words or several paragraphs. map groups the clickable areas of an image map along with whatever content surrounds them. And an a may be a word inside a sentence or a whole card on a page.

That last point was a change. In HTML 4 the DTD declared A as inline only, so a link around a heading and a paragraph was invalid, though every browser rendered it. HTML5 made a transparent and so made the block link legal. The link, in the Linking section, follows the block link through its accessibility trade-offs and shows the nested-link repair in detail.

What the parser does with invalid markup

A content-model error never stops the browser, and the cases fall into two groups. In the first, the parser restructures the tree, so you get a tree you did not write. In the second, it builds the invalid structure exactly as written. Each claim below was checked in Chrome by parsing the snippet as a document; the demo after the list does the same live.

Errors the parser restructures

  • A block inside a paragraph. In <p>Intro <div>a block</div> after</p>, the div start tag closes the paragraph, so the paragraph holds only "Intro," the text " after" lands loose in the body, and the final </p>, with no paragraph open, creates a new, empty one. A ul, a table or a heading does the same (in no-quirks mode; quirks mode lets a table sit inside a paragraph).
  • A link inside a link. The parser never builds one. A second <a> start tag while a link is open closes the first link. In a simple case the result is two links side by side, with any text after the inner link left outside both.
  • A button inside a button, a form inside a form. A second <button> start tag closes the open button, giving two buttons in a row. A <form> start tag inside an open form is ignored altogether, and its fields join the outer form.
  • Content in the wrong place in a table. Text or an element such as p that appears directly in a table, outside any cell, is moved out and inserted just before the table. The standard calls this foster parenting. Its own example of an error with an "unintuitive" result is <table><hr>, which puts the hr before the table. Rows written directly in a table also get a tbody the source never mentioned.

The first case is worth seeing as a tree, because it is common and its repair changes three things at once.

A div inside a paragraph, as meant and as builtOn the left, the tree the author meant: body contains one p, and the p contains the text Intro, a div holding the text a block, and the text after. On the right, the tree the parser builds: body contains four children in a row. A p holding only the text Intro; the div holding a block; the text after, loose in the body; and a second p, empty, marked in red, created by the end tag.As meantbodyp"Intro "div" after""a block"As builtbodypdiv" after"p"Intro ""a block"empty,from </p>one paragraph became two, and " after" belongs to neither
The div start tag closes the paragraph; the stray end tag opens an empty one. A rule such as p { margin-block: 1rem } now spaces two paragraphs, and p > div matches nothing.

Errors the parser builds as written

  • A div or text inside a ul. The tree holds the div or the text as a child of the list, just as written. There is no rule that moves it.
  • A button inside an a, or a link inside a button. Both elements stay nested in the tree, and the browser has to decide which one a click activates.
  • A li outside any list. The parser does not invent a list for it. The li sits in its parent, styled as a list item by the browser's default style sheet, but it belongs to no list, and assistive technology has no list to announce.
  • A div inside a span, or inside an em. A block start tag closes an open paragraph, but not an open span or em: those stay open and simply contain the block.

The second group is the more dangerous, because nothing looks wrong: DevTools shows the tree you wrote, and the page looks right. Only the validator objects.

Tag soup and the soup parse

"Tag soup" is the old name for markup like this: tags unclosed, overlapping or unknown, in pages that work anyway. The course slides ask whether that is HTML's strength or its weakness, and the answer is both. It is why a page from 1995 still renders. It is also why, for most of the web's history, the same broken page built different trees in different browsers, each with its own error handling, so a script could work in one browser and fail in the next.

HTML5's answer was not to forbid soup. It was to define it. The parsing chapter of the standard specifies, for every possible input, which tree comes out; in its own words, "the error handling for parse errors is well-defined." Today every major engine implements that one algorithm; the Nu HTML Checker's parser, by Henri Sivonen, is even the source Firefox's HTML parser is translated from. The course's author calls this consistent "soup parse" the single largest win of the HTML5 effort: "at least we have predictability in the face of junk markup." The snippets in the demo above build identical trees in Chrome and in WebKit, the engine behind Safari.

Predictable is not the same as correct, and it is not the same as strict. HTML5 also made a deliberate return to loose valid syntax:

<!doctype html> <!-- no html, head or body tags: all three are optional --> <meta charset=utf-8> <title>Loose and valid</title> <H1 class=intro>Case is no problem</h1> <p>Quotes are optional when a value has no spaces <p>and end tags can be inferred <ul> <li>like these <li>list items </ul>

The Nu HTML Checker reports no errors for it. Its one remark is a warning to add a lang attribute to the html start tag, which is a good reason to write that tag after all. Nothing here relies on error recovery: the omitted tags and missing quotes are part of the grammar. Writing this way is a style choice, and most teams close and quote everything because it reads and diffs better. Hold on to the difference between loose, which the grammar allows, and invalid, which only the error handling copes with. Both render. Only one means what it says.

The deeper problem is that the decision is rarely made on purpose. The slides picture a developer between two voices: "who cares, it will work in the browser," and HTML that "might be work, but it parses fast, helps SEO and is a11y friendly." The developer, keen to get to the JavaScript, never decides, and the soup accumulates. A validator in the workflow makes the decision once.

Why validity still matters

If every browser builds the same tree from the same soup, why bother being valid? The standard asks the question itself, and answers that "interoperability ... is not the only goal of document conformance requirements." Four reasons carry most of the weight.

The tree you get may not be the tree you meant. The repair follows the parser's rules, not your intent. The paragraph that became two and the text fostered out of a table are correct output for the input, and wrong output for the author.

Everything downstream reads the tree. CSS selectors match the tree, so p > div or table > tr silently match nothing. Scripts query the tree, so querySelector returns null for an element that is plainly in the source. The accessibility tree is computed from it, so a div in a list is something that is neither an item nor outside the list, and screen readers differ on how they announce a button inside a link. Search engines and AI agents read it too. The Accessibility page follows that thread through the course.

Invalid structures have no good behavior to rely on. The standard explains several of its rules this way. Interactive content cannot be nested because "the default behavior of such nesting interactive elements would be highly confusing to users"; a ul may contain only lis because if it contains anything else, "it's not clear what was meant." When the meaning is unclear, each browser, screen reader and crawler picks its own interpretation, and they need not agree.

Valid markup is easier to maintain. In a codebase that validates cleanly, a new error stands out the day it appears. A codebase with three hundred errors hides the three hundred and first.

The slides warn against jumping to conclusions, though. Famous home pages rarely validate. On the day this lesson was written, the version of Google's home page that the Nu HTML Checker received produced eight errors, among them the obsolete <center> element, and the HTTP Archive's Web Almanac 2024 found <center> and <font> each on 4.5% of the mobile pages it measured. Did those authors do something on purpose? Sometimes a team trades validity for bytes or an old browser, knowing the cost. The point is that the trade should be a decision, not an accident nobody looked at.

The validator as a linter

Treat the validator as a type checker or linter: a fast, automatic reader that runs before your code reaches anyone else. Two are worth knowing.

One source file, two readersA box labeled page.html on the left has two arrows. The upper arrow leads to the browser's HTML parser, which never fails and repairs silently, and then to a tree, always, which CSS, scripts and assistive technology read. The lower arrow leads to a validator, which applies the same parsing rules plus the content models and attribute rules, and then to a red box: a list of errors with line numbers. A note says only the lower path tells the author anything.page.htmlbrowser's HTML parsernever fails, repairs silentlya tree, alwaysread by CSS, scripts, ATvalidatorparsing rules + content modelserrors, by lineonly this path tells you
The browser and the validator read the same file. The browser always produces a tree and says nothing; the validator produces a report. Only the second reader is optional, which is why it gets skipped.

The Nu HTML Checker

The Nu HTML Checker is the W3C's validator for current HTML, at validator.w3.org/nu. It checks a page by address, by uploaded file or as pasted text. Its parser implements the standard's algorithm, so it reports parse errors exactly where a browser's error handling kicks in, then checks the tree against the content models and attribute rules, and checks the CSS too. The same checker runs offline as vnu.jar, as pre-built binaries, and as the npm package vnu-jar, for build scripts and continuous integration. (The older Markup Validation Service at validator.w3.org/check still checks the old DTDs; given an HTML5 document, it points to the Nu checker.)

An error is a conformance failure; a warning is advice, such as "Section lacks heading." A few errors are fatal: text directly inside a table stops the checker with "Cannot recover after last error. Any further errors will be ignored." The standard requires a checker to report at least one parse error, not all of them, so after a fatal error, fix it and check again.

html-validate in the editor and in CI

html-validate is a separate project, an offline validator written in JavaScript and installed from npm. Its own parser is strict and makes no corrections. It checks fragments as well as documents, including markup inside framework components, and runs in editors and CI with no network connection. Its recommended preset adds rules beyond the standard, such as a type on every button; its standard preset sticks to conformance.

The two overlap without agreeing. On the practice page below, both find the stray </p>, the button in a link, the duplicate id, the paragraph in a list and the missing alt; only Nu finds the bad datetime value and the skipped heading level; html-validate adds the button with no type, legal but a common bug, and a paragraph closed implicitly by a list, which is valid HTML that the recommended preset flags as a style choice. Given text directly in a table, html-validate's standard preset reports nothing.

What each tool is for
Nu HTML Checkerhtml-validate
RulesThe standard: parse errors, content models, attribute values, obsolete features, plus CSSContent models and many standard rules, plus optional style and accessibility rules
ParserThe standard's algorithm, the same one browsers useIts own strict parser, no error recovery
InputWhole documents, by address, file or pasteDocuments or fragments, including component templates
Best placeBefore you publish, and as the reference when tools disagreeIn the editor and on every commit

Neither tool checks meaning. A page built entirely of divs passes both, and so does a link whose text is "click here." Validation catches type errors, as a compiler does, and leaves meaning to you and the next lesson.

Reading the messages

A message names what the validator saw, at the point where it noticed, which is often after the mistake. Three habits help.

  • Fix the first error first, then check again. One unclosed element can cause a cascade of later messages that all disappear together.
  • Look one step back from the line it names. For <p><div>…</div></p>, Nu says "No p element in scope but a p end tag seen," and points at the </p>. The mistake is the div that closed the paragraph before it.
  • Translate the message into a content-model question. "Element div not allowed as child of element ul in this context" means: look up ul's content model. Every content-model message is that lookup in disguise.

Code, validate, preview

The course's tooling advice describes a loop: write, validate, preview, repeat. Keep it fast enough to run constantly, using, in the slides' words, "rough sand paper as opposed to finishing paper" while you build:

  1. While writing, let an editor integration of html-validate underline errors as you type, the way a spelling checker does.
  2. On every commit, run a validator over the built pages in CI and fail the build on errors, not warnings. A failed build is the one reminder nobody forgets.
  3. Before publishing, check the real pages with the Nu HTML Checker, by address: it sees what the browser sees.
  4. Preview in more than one browser. The tree will match; the rendering, the fonts and the form controls may not.

A tool chain, the slides warn, "should be supportive and appropriate to the level you are at." For a course project, the Nu checker and a habit are enough.

Try it: find the seven errors

The page below looks fine in every browser. It holds seven deliberate errors, each a rule from this lesson.

  1. Before running anything, read the source of validator-practice.html (View Source, or the demo's own tab) and list every error you can find by eye, with the rule each one breaks.
  2. Check the page with the Nu HTML Checker at validator.w3.org/nu: choose "address" and paste https://cse134.site/structure/html/demos/validator-practice, or paste the source as text. Compare its report with your list. Which did you miss, and which did you find that it does not report?
  3. For the stray </p> error, open DevTools on the practice page and find the empty paragraph the parser made. Then explain why the message points at the end tag and not at the list.
  4. Fix every error in a copy of the page and check again until the report is empty. Then open "Show the answers" at the bottom of the page and compare your fixes with the suggested ones.
  5. Run your own most recent project through the checker. Pick one error, work out the content-model rule behind it, and predict what tree the browser built. Check the prediction in DevTools.

If you have Node installed, run npx html-validate validator-practice.html on a saved copy as well, and compare its list with Nu's. The differences are the subject of the table above.

Summary

ConceptKey points
Well-formed and validWell-formed is the surface grammar; valid adds the rules for which element goes where, with which attributes. HTML's grammar allows omitted tags and unquoted values, so loose markup can be valid. The standard's word is conforming.
The DTD eraHTML 2.0 to 4.01 were SGML applications defined by DTDs: - O for an omissible end tag, (LI)+ for a list's content, -(A) to exclude nested links. Only validators parsed HTML as SGML; browsers never did.
The living standardNo DTD. Each element's entry states its categories, contexts and content model in prose, and the parsing chapter defines the tree for every input. The doctype remains for legacy reasons.
Content categoriesMetadata, flow, sectioning, heading, phrasing, embedded, interactive; plus palpable and script-supporting. Overlapping sets, read like types: phrasing is a subtype of flow. li, tr, td, option, dt, dd have no category and live only in their parents.
Reading an entryCategories: what it is. Contexts: where it goes. Content model: what it accepts.
TransparentContent must be valid as if the element were not there. a, ins, del, map, object, canvas, slot, and the fallback in audio and video. a adds: no interactive content, no a, no tabindex inside.
Restructured errorsBlock in p closes it; nested a or button closes the outer; nested form is ignored; misplaced table content is foster-parented before the table.
Errors built as writtendiv or text in ul, button in a, li outside a list, div in span. Nothing looks wrong; only a validator says so.
Tag soupHTML5 defined error handling, so every browser builds the same tree from the same soup. Predictable is not correct. Loose valid syntax is a style choice; invalid markup is an accident.
Why botherThe repaired tree may not be the one you meant, and CSS, scripts, assistive technology and crawlers all read the tree. Invalid structures have no reliable meaning.
The validatorsNu HTML Checker: the reference, with the browsers' parser. html-validate: offline, strict, for the editor and CI. Neither checks meaning. Fix the first error first, and read each message as a content-model lookup.

Next steps

This lesson ends the Principles group: HTML as a language, the document, its syntax, and now its type system and the tool that checks it. A valid document can still say very little, though. Lesson 05, Landmarks and sectioning, starts the Semantics group with the question validation cannot answer: of all the valid elements, which one says what this part of the page actually is? The parser's side of this lesson, the tokenizer and tree construction behind every repair shown here, is covered in depth in Parsing HTML into the DOM, and CSS's own, quite different, forgiving parser in CSS Basics.