1. Structure
  2. HTML
  3. The document

Lesson 02 · Principles

The document

Every HTML file starts with the same few lines, and most authors paste them in without reading them. Each one has a job. The doctype picks a rendering mode. The charset tells the browser how to turn bytes into characters. The server's MIME type decides which parser reads the file at all. The lang attribute tells every reader, human or machine, what language the words are in. Get one wrong and the page still appears, slightly broken, with no error anywhere. This lesson reads those lines one at a time.

Learning objectives

  • Name the parts of a minimal HTML document and say what each one is for
  • Predict the tree the parser builds when html, head and body tags are left out, including where the head ends
  • Explain what the doctype still does, and tell the three rendering modes apart by what you can see and by document.compatMode
  • Explain how a browser decides a page's character encoding, and diagnose mojibake from the characters it produces
  • Explain why the MIME type the server sends, not the file's extension, decides how the markup is parsed
  • Set lang and dir on the root element, and know what depends on them

The skeleton

Lesson 01 showed that a browser will build a page from a file with no tags at all. That tells you what the parser tolerates, not what you should write. Here is the document this course asks you to start from:

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Course schedule · CSE 134B</title> </head> <body> <h1>Course schedule</h1> </body> </html>

The slides for this course label the parts as a stack: the doctype, which "traditionally indicates the version of HTML used"; the root element; the head, which holds information about the document, where you "always set the character set first" and where the "document title is mandatory"; and the body, which holds the content.

The parts of a minimal documentA stack of nested boxes. At the top, outside everything, the doctype, labelled "chooses the rendering mode". Below it the html element with lang, labelled "the root; the document's language". Inside html, the head, containing meta charset, labelled "how to decode the bytes, first", meta viewport, labelled "how to size the page on phones", and title, labelled "required; names the page". Below the head, still inside html, the body, labelled "everything people see".<!doctype html>chooses the rendering mode<html lang="en">the root; the document's language<head>about the page; nothing here renders<meta charset="utf-8">how to decode the bytes: first<meta name="viewport">how to size the page on phones<title>required; names the page<body>everything people see
The skeleton. The doctype sits outside the tree's root; everything else is inside html, split into a head that configures and a body that renders.

The doctype is not an element. It is a preamble that the parser reads before anything else, and the tree records it as a separate node, document.doctype, outside the root. Its one remaining job is covered below.

The html element is the root: every other element in the document is its descendant. It does little work itself, but its lang attribute states the language of everything inside, and it is the only place to state the language of the whole document.

The head is the document's metadata: its title, its encoding, its style sheets and scripts, its relationships to other documents. Nothing in it renders as content. Search engines, social previews, browser tabs, bookmarks and screen readers announcing a newly loaded page all read it. <title> is the head's only required child. Lesson 09 covers the head element by element.

The body holds the content, which is the subject of most of this series. A document has exactly one head and one body, always, whether or not you typed them, which is the next section's point.

Tags you may leave out

Lesson 01's five-line example produced a full html/head/body tree from a file that typed none of them. That was not error recovery. Leaving out these tags is valid HTML. The specification lists, for each one, exactly when its start or end tag may be omitted, and its first sentence on the subject is the important one: omitting a tag "does not mean the element is not present; it is implied, but it is still there."

TagMay be left out when
<html>the first thing inside the html element is not a comment
</html>the element is not immediately followed by a comment
<head>the head is empty, or the first thing inside it is an element
</head>the head is not immediately followed by whitespace or a comment
<body>the body is empty, or the first thing inside it is not whitespace or a comment, and is not a meta, noscript, link, script, style or template element
</body>the body is not immediately followed by a comment

The conditions look fussy, and all of them say the same thing: you may leave a tag out wherever the parser would put the element back in the same place. The exceptions are the cases where leaving it out would move something. Omit <html> before a comment and the comment ends up outside the root. Omit <body> before a <script> and the script lands in the head instead, because a script is allowed there.

Where the head ends

The parser decides where the head ends by watching for the first thing that cannot go in a head. Elements such as meta, title, link, style, script and base keep the head open. Whitespace and comments keep it open too. The first non-whitespace text, or the first element that belongs in a body, closes the head, opens an implied body, and is placed there. Here is a head with one stray word in it:

<!doctype html> <title>Menu</title> <meta charset="utf-8"> Hello <link rel="stylesheet" href="menu.css"> <meta name="description" content="Today's specials"> html ├── head │ ├── title "Menu" │ └── meta charset └── body ├── #text "Hello" ├── link rel=stylesheet └── meta name=description

The word closed the head, so the link and the description ended up in the body. The style sheet still applies, since browsers honour a style sheet link wherever it appears, but the description is now a meta element in the body, where it is invalid and where a crawler looking in the head will not find it. An unknown element, such as a misspelled <meat> or a custom element, has the same effect as the stray word. The source looks like a head; the tree says otherwise. As lesson 01 put it, the tree is what everything reads.

The demo below takes the rule to its limit. Its source has a doctype and an <html lang="en"> start tag, then no head or body tags at all, and a script at the end prints the tree the parser built from it:

So should you write the tags? Some style guides, Google's among them, recommend leaving optional tags out to save bytes. This course writes them. The <html> start tag is the only place to put lang, so in practice it is never optional, and writing <head> and <body> makes the boundary visible, so a stray word in the head is easy to spot. Compression removes most of the saving anyway.

The doctype

The modern doctype is fifteen characters, <!doctype html>, case-insensitive. To see why it exists at all, go back to where HTML came from.

From DTDs to fifteen characters

Lesson 01 described SGML, a language for defining languages, and the Document Type Definition, the DTD, that declares a language's elements and how they may nest. An SGML document announces which DTD it follows in its first line, the document type declaration, which is where the word "doctype" comes from. In the slides' terms, the doctype identifies "the markup dialect used in a document by referencing an external DTD." HTML 4.01, the last SGML version of HTML, came in three dialects, and each had its own doctype:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">

Each has two identifiers. The quoted string after PUBLIC is a formal public identifier: a name, in a fixed format, that says who owns the DTD (W3C), what it is (a DTD), which one (HTML 4.01 Transitional), and its language (EN, the language of the DTD itself, not of your page). The second string is a system identifier, a URL where the DTD file can be fetched. Strict left out the presentational elements and attributes such as <font> and <center>; Transitional kept them for pages moving to CSS; Frameset was for pages built from frames. XHTML 1.0, in 2000, reformulated the same three as XML, with the same three doctypes in XML form:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

Here is the surprise: no browser has ever read those DTDs. Browsers were never validating SGML parsers and never fetched the URL. The DTD was for validators, which used it to check a document against its declared dialect.Other software did fetch them. In February 2008 the W3C published a post titled "W3C's Excessive DTD Traffic", reporting that XML tools and libraries were requesting the DTDs on its site as often as 130 million times a day, many of them on every run, and asked developers to cache them.What browsers did read was the doctype string itself, for a reason that had nothing to do with SGML.

The doctype switch

By the late 1990s millions of pages relied on the bugs of Netscape 4 and Internet Explorer 5, so a browser that fixed the bugs to follow the CSS specification would break them. Internet Explorer 5 for the Macintosh, released in 2000, found a way out. Old pages rarely had a doctype; pages written by authors who knew the standards usually did. So the browser looked at the doctype and chose: a known, modern doctype got the standards-following behavior, and a missing or old one got the old, bug-compatible behavior, which came to be called quirks mode. Mozilla adopted the idea, Internet Explorer 6 for Windows followed in 2001, and every browser has done it since. The slides call this "the doctype switch."

That switch is the doctype's only job in a browser today. HTML5 dropped the DTD, because HTML is no longer defined in SGML, and kept the doctype only because a page without one is rendered in quirks mode. The specification says so directly: "DOCTYPEs are required for legacy reasons." <!doctype html> has no version and no URL; it is short, easy to remember, and was already enough to switch every browser of the time into standards mode. There is no version number to update, because there is only one HTML. For the rare tool that cannot write the short form, the specification allows <!DOCTYPE html SYSTEM "about:legacy-compat">, which means the same thing.

Three rendering modes

The specification now defines the switch exactly, as the first thing the parser does. There are three modes.

  • No-quirks mode (once called "standards mode"): HTML and CSS behave as their specifications say. <!doctype html> selects it, and so do the HTML 4.01 Strict and XHTML 1.0 Strict doctypes.
  • Limited-quirks mode (once called "almost standards mode"): no-quirks mode with one difference, how the height of a line is calculated around images, which keeps old image-sliced layouts from opening gaps. The XHTML 1.0 Transitional and Frameset doctypes select it, as do the HTML 4.01 Transitional and Frameset doctypes when they include the DTD's URL.
  • Quirks mode: a list of deliberate bugs, emulating the browsers of the late 1990s. A missing doctype selects it, and so does any of about sixty old doctypes on the specification's list, including HTML 4.01 Transitional without its URL, and text or an element before the doctype.
How the parser chooses a rendering modeThe first thing in the file other than whitespace and comments leads to one of three modes. If it is not a doctype, or it is a doctype on the specification's legacy list, such as HTML 3.2 or HTML 4.01 Transitional without a URL, the result is quirks mode, where document.compatMode is BackCompat. If it is XHTML 1.0 Transitional or Frameset, or HTML 4.01 Transitional or Frameset with a URL, the result is limited-quirks mode, where compatMode is CSS1Compat. Anything else, including doctype html and the Strict doctypes, gives no-quirks mode, where compatMode is also CSS1Compat.first thing in the fileafter any whitespace and commentsno doctype, or an old one:HTML 3.2, HTML 2.0,4.01 Transitional, no URLtransitional with a URL:XHTML 1.0 Transitional,4.01 Transitional + URLanything else:<!doctype html>,4.01 and XHTML StrictquirksBackCompatlimited-quirksCSS1Compatno-quirksCSS1Compat
The doctype switch as the parser runs it. The value under each mode is what document.compatMode reports; it cannot tell limited-quirks from no-quirks.

Two details are worth knowing. A comment before the doctype is harmless; text before it is not. A file that begins with a stray word, such as a debugging message printed by a server script before the template, is in quirks mode, because the doctype was not the first thing the parser saw. And you can always ask a page which mode it is in: document.compatMode returns "BackCompat" in quirks mode and "CSS1Compat" otherwise. Type it into the console of any page. Firefox also prints a console warning when a page is in quirks or limited-quirks mode.

What quirks mode changes

The quirks are written down, in the WHATWG's Quirks Mode Standard and the HTML specification, so that every browser emulates the same old bugs in the same way. The demo below loads two files that are identical except for line 1, which is <!doctype html> in one and missing from the other. Each frame prints its own document.compatMode.

Five differences show, each one a quirk you could meet in old code:

  1. Tables forget the font. In quirks mode a table does not inherit the font size, weight or style of its surroundings; the browser's style sheet resets them at every <table>. Old sites therefore repeated <font> tags inside every table cell. In no-quirks mode the table inherits like any other element.
  2. CSS mistakes are forgiven. width: 200 has no unit and background-color: fd8 has no #. Both are invalid, and no-quirks mode drops them. Quirks mode reads them as pixels and hexadecimal, but only for a fixed list of older properties: width gets the help, inline-size does not.
  3. Percentage heights work. In no-quirks mode, height: 12% of a parent whose height comes from its content means nothing, so it is ignored. Quirks mode keeps looking up the tree for something with a height and ends at the window; it also stretches body to fill the window. Hence the old advice to set html, body { height: 100% } first.
  4. Inputs use the other box model. In quirks mode, text inputs and text areas measure their width from border edge to border edge, as if they had box-sizing: border-box; in no-quirks mode they measure the content alone and the padding is added on. The demo measures both.
  5. No gap under images. An image sits on a text baseline, so in no-quirks mode there is room under it for letters' descenders, a gap of a few pixels inside the box. Quirks and limited-quirks modes drop the gap. That is the one quirk limited-quirks mode keeps.

The fourth item corrects a claim you will still find in older tutorials: that quirks mode switches the whole page to "the IE box model," where a div's width includes its padding and border. Internet Explorer 5 did size every box that way, and Internet Explorer's own quirks mode kept doing so for years. Current browsers keep that behavior only for text inputs, text areas and the heights of table cells; a div or a paragraph is sized the same in all three modes. In the demo the paragraph is the same width in both frames, and only the input changes. Today you choose the box model yourself with box-sizing, as CSS layout explains, and it works the same in every mode.

None of these is useful in new work. Quirks mode exists so that pages from 1998 keep working, and it applies to any page that forgets its doctype.

Character encoding

Lesson 01 described the parser as a pipeline that starts with bytes, and the first stage is the one most authors never think about: turning bytes into characters. A file on disk or a response on the network is a sequence of bytes, numbers from 0 to 255. Text is a sequence of characters. A character encoding is the rule that maps one to the other, and the same bytes mean different text under different rules. The parsing module of How Browsers Work calls this "the first place a page can be silently wrong before a single tag has been recognized."

One encoding: UTF-8

ASCII, from the 1960s, defines 128 characters: enough for English, not for "café". Dozens of regional encodings extended it, such as windows-1252 for Western Europe, windows-1251 for Cyrillic and Shift_JIS for Japanese, each giving the same byte values different meanings, so a file made sense only if you knew which one it used.

Unicode replaced the patchwork with one numbered list of characters, over 150,000 of them, covering every modern script and emoji. UTF-8, designed by Ken Thompson and Rob Pike in 1992, is the way to write those numbers as bytes. It uses one byte for each ASCII character, the same byte ASCII uses, so plain English text is identical in both, and two to four bytes for everything else: é is C3 A9, € is E2 82 AC, and the grinning-face emoji is F0 9F 98 80. According to W3Techs, in October 2026, 99.1% of websites whose encoding it knows use UTF-8. The HTML specification goes further: a document "must" be encoded in UTF-8, and authoring tools must default to it. There is no decision to make. Save your files as UTF-8, and say so.

Declaring it

Saying so is the job of <meta charset="utf-8">. The declaration has a chicken-and-egg problem: it is written in the bytes it describes. HTML gets away with it because the declaration is pure ASCII, which reads the same in nearly every encoding, and because browsers look for it early. Before parsing starts, the browser prescans the start of the file for a meta charset. The specification requires the declaration to be complete within the first 1024 bytes, and that is why the slides say to "always set the character set first": make it the first element in the head, before the title, which may itself contain non-ASCII characters. A declaration found later may cause the browser to throw away what it has parsed and start again with the new encoding, or it may be missed entirely; testing in Chrome for this lesson, one after a long comment in the head was honoured, and one after some body text was ignored.

The browser takes the first answer it finds, in this order:

  1. A byte order mark. A file may begin with the bytes EF BB BF, an invisible marker that means "UTF-8". If it is there, it wins over everything else, including a meta that says otherwise.
  2. The HTTP header. Content-Type: text/html; charset=utf-8 on the response. Many servers send it; many, including the one serving this course's demos, send text/html with no charset, which leaves the decision to the page.
  3. The meta declaration, found by the prescan.
  4. A guess. A frame may inherit its parent page's encoding; the browser may remember the page from an earlier visit, or analyse the bytes. Failing all that, it uses a default that depends on the browser's locale. For English, and most other locales, the default is windows-1252.

The last step matters. A page with no declaration, served without a charset, is decoded as windows-1252 on an English-language computer and as something else on a Japanese or Russian one. The specification requires a declaration even for a page that is entirely ASCII, because the encoding also governs what happens to text people type into its forms and to URLs a script builds from it (the URLs section covers percent-encoding).

Mojibake

Get the declaration wrong and you get mojibake, from the Japanese for "character transformation": text decoded with the wrong rule. It has a recognizable look, and once you can read it you can diagnose it. Take "café" saved as UTF-8 and decoded as windows-1252, which is a single-byte encoding. Every byte becomes one character, so the two bytes of é become two characters:

The same bytes decoded two waysFive bytes in the middle row: 63, 61, 66, C3, A9. Decoded as UTF-8, shown above, the first three bytes become c, a and f, and the last two together become one character, e acute, giving "café". Decoded as windows-1252, shown below, every byte becomes its own character: c, a, f, then C3 becomes A with a tilde and A9 becomes the copyright sign, giving "café".read as UTF-8café"café"bytes in the file636166C3A9read as windows-1252café"café"two bytes, one characterone byte, one character
The bytes do not change; the rule for reading them does. UTF-8 reads C3 A9 as one character. windows-1252 reads it as two.

The pattern generalizes. UTF-8 read as windows-1252 turns each accented Latin letter into two characters, usually starting with Ã. Curly quotes, apostrophes and dashes are three bytes in UTF-8 and come out as three characters starting with â€: "it’s" becomes "it’s", the most common mojibake on the web. Japanese and emoji turn into longer runs of accented letters and symbols. The opposite mistake, a windows-1252 file read as UTF-8, looks different: its accented bytes are not valid UTF-8, so each one becomes the replacement character, �, and "café" becomes "caf�". Seeing à and †means UTF-8 was read as something else; seeing � means something else was read as UTF-8.

The demo's bytes are UTF-8, written deliberately by a script, and its meta declares windows-1252. The explanations on it are ASCII, so they survive; the samples do not.

Mojibake often comes from outside the page, wherever text crosses a boundary that assumes another encoding: a database connection, a spreadsheet export, a copy and paste. The fix is the same everywhere: UTF-8 at every step, declared wherever it can be.

The MIME type

Before the parser can decode anything, the browser has to know which parser to use, and it does not decide by looking at the file name. It reads the Content-Type header of the HTTP response, which carries a MIME type (also called a media type): text/html for an HTML page, text/css, image/png, application/json, and so on. The server chooses the header, usually from a table that maps extensions to types; the extension matters only because the server's configuration says so.

The slides for this course call this "not a detail" and show why. An HTML file served as text/plain is displayed as text, tags and all, and that is what current browsers do. Old versions of Internet Explorer instead sniffed the content, decided it looked like HTML, and rendered it. The slides name this "Thomas' Law of Unintended Consequences": people don't know MIME, and trouble follows when software "fixes" things for them. Sniffing also turned out to be a security problem, since an uploaded "image" that a browser decided was HTML could run script, and today servers can forbid it with X-Content-Type-Options: nosniff.

Same markup, two parsers

The MIME type matters most for one pair. Served as text/html, a document goes to the HTML parser, the forgiving one lesson 01 described. Served as application/xhtml+xml, the same bytes go to an XML parser, which is strict: the first well-formedness error stops it. The file below has one unclosed <em> and one self-closed <div/>:

<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" lang="en"> <head><meta charset="utf-8" /><title>Same markup</title></head> <body> <p>One <em>unclosed emphasis.</p> <div/> <p>After the div</p> </body> </html>

As application/xhtml+xml, Chrome shows a pink box reading "This page contains the following errors: … Opening and ending tag mismatch: em line 5 and p", followed by "a rendering of the page up to the first error", which is the word "One". As text/html, the page renders in full. The HTML parser closes the em at the end of the paragraph and then reopens it, so the rest of the page is in italics. It ignores the slash in <div/>, so the div stays open and swallows the paragraph after it. Neither result is what the author meant, but only one of them tells you.

Other differences follow from the parser. XML is case-sensitive, so in XHTML <P> is not a paragraph but an unknown element; in HTML it is a paragraph. XHTML documents are always rendered in no-quirks mode, doctype or not. And in XHTML, <div/> really is an empty div.

This is why lesson 01 said that most XHTML on the web never was XHTML. Authors wrote XML-style markup and served it as text/html, partly because Internet Explorer before version 9 could not display application/xhtml+xml at all, so the HTML parser read every page and checked nothing. As the slides put it, "HTML5 doesn't mean XHTML is dead": its strictness is available to anyone who serves it with the right type. The doctype never decided which language a page was in; the MIME type did. For this course, everything is text/html, and the trailing slashes are optional decoration that the parser throws away.

Language and direction

The lang attribute on the root element states the primary language of the document, as a language tag: en, en-GB, es, ja, zh-Hant. It is one of the most useful attributes in HTML and one of the most often forgotten, because a page without it looks exactly the same to its author. What depends on it:

  • Pronunciation. Screen readers choose a voice and pronunciation rules from it. Without it they fall back to the language of the user's system, so an English reader's screen reader reads a Spanish page with English rules. This is why WCAG makes a programmatically determined page language a Level A requirement (success criterion 3.1.1), the lowest bar there is.
  • Hyphenation and line breaking. CSS hyphens: auto only hyphenates text whose language the browser knows; on a page with no lang, or an unknown one, nothing is hyphenated. It also decides the glyphs used for Chinese and Japanese characters, which share code points but differ in form.
  • Translation. Translation tools use it to know what to translate from.
  • Spelling and input. The language of an editable field tells the browser which language the user is expected to type, which spell checkers can use.
  • Styling. The :lang() selector lets CSS choose per language, for example the quotation marks q elements use: “ ” in English, « » in French.
  • Not search, at least not Google. Google's documentation says it determines a page's language from the visible text and does not use code-level language information "such as lang attributes". Write lang for people and their tools, not for ranking.

Set it on html and it covers everything, including the title, which is announced when the page loads. Set it again on any element whose content is in a different language, such as a quotation or a name: <span lang="fr">. Use the right tag, not a guess; a page in Spanish marked lang="en" is worse than one with no lang, because it is confidently wrong. The film The Web Speaks Every Language. Does Your Page? shows the cost of getting language wrong.

The dir attribute sets the base direction of the text: ltr, the default, or rtl, for Arabic, Hebrew, Persian, Urdu and other right-to-left scripts. Put dir="rtl" on html for a right-to-left page and the whole layout mirrors, including CSS logical properties such as margin-inline-start, which is one reason this course writes them instead of margin-left. For text whose direction you do not know in advance, such as a user's name, dir="auto" lets the browser decide from the first strong character. Direction is not implied by lang; an Arabic page needs both.

The viewport, in outline

The last line of the skeleton is for phones:

<meta name="viewport" content="width=device-width, initial-scale=1">

Without it, mobile browsers assume the page was designed for a desktop screen, lay it out at a width of about 980 CSS pixels, and shrink the result to fit, which gives tiny text that people have to zoom. With it, the page is laid out at the width of the device. Never add user-scalable=no, which stops people from zooming. Lesson 09 covers the viewport in detail.

A minimal correct document

Here is the skeleton again, with each line's reason, as a template to start every page from:

<!doctype html> <!-- no-quirks mode --> <html lang="en"> <!-- the language of everything below --> <head> <meta charset="utf-8"> <!-- first, inside the first 1024 bytes --> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Page name · Site name</title> <!-- required, specific --> </head> <body> <!-- content --> </body> </html>

Three things go with it that are not in the file: save it as UTF-8, serve it as text/html (with ; charset=utf-8 if you control the server), and check it in the Nu HTML Checker. Style sheets, scripts, descriptions and icons are added to this skeleton, not instead of it.

Try it: read the first lines

Four exercises, each about ten minutes:

  1. Open render-modes.html. For each of the five tests, write one sentence saying what differs and why. Then open any page of a site you use and run document.compatMode in the console. If you find one that says BackCompat, View Source and find out why.
  2. Save a small page containing "café", "it’s" and an emoji as UTF-8 with <meta charset="windows-1252">, open it, and predict each piece of mojibake before you look. Then change the declaration to utf-8. Then remove it entirely and open the file again; note what document.characterSet reports, and whether the browser guessed right.
  3. Write a document with no <head> or <body> tags, with a title, then a word of text, then a <meta name="description">. Predict the tree, then check it in the Elements panel. Move the word below the meta and check again.
  4. Pick three sites in three languages. In the console, check document.documentElement.lang and document.dir on each. Note any that are missing or wrong, and what a screen reader would do with them.

Summary

ConceptKey points
The skeletonDoctype, html lang, head with meta charset first, the viewport and a title, then body. The title is the head's only required child.
Optional tagshtml, head and body tags may be omitted where the parser would put the element back in the same place; the elements are always in the tree. Keep <html> for lang.
Where the head endsAt the first non-whitespace text or body element. Anything after it lands in the body, even meta and link.
The doctypeOnce named an SGML DTD that browsers never read. Its only job now is choosing the rendering mode. Write <!doctype html>, first.
Rendering modesNo-quirks (<!doctype html>, Strict doctypes), limited-quirks (Transitional with a URL, XHTML Transitional: only the line-height quirk), quirks (no doctype, old doctypes, text before the doctype). document.compatMode: BackCompat or CSS1Compat.
QuirksTables reset fonts; unitless lengths and hashless colours accepted; percentage heights resolve against the window; inputs use border-box; no gap under images. Not "the IE box model" for every element.
EncodingBytes become characters by a rule. Use UTF-8. Order: byte order mark, HTTP charset, meta in the first 1024 bytes, then a guess (windows-1252 for English).
Mojibakeé and ’: UTF-8 read as windows-1252. �: something else read as UTF-8. The declaration labels bytes; it does not convert them.
MIME typeThe server's Content-Type picks the parser, not the extension. text/html: forgiving HTML parser. application/xhtml+xml: XML parser, stops at the first error.
lang and dirlang drives pronunciation, hyphenation, translation, spell checking and :lang(); WCAG Level A. Google ignores it. dir="rtl" mirrors the layout; dir="auto" for unknown text.
Viewportwidth=device-width, initial-scale=1; never disable zoom.

Next steps

The document is now in place: the right mode, the right encoding, the right parser and a language. Lesson 03, Elements, attributes and entities, looks inside it at the grammar of the markup itself: how tags, attributes and character references are written, which elements are void or hold raw text, what the parser does with whitespace, comments and names it does not know, and the global attributes, lang and dir among them, that every element accepts.