../

Document & head

Everything above <body>: the doctype, the root element, and the <head> that tells browsers, crawlers and link unfurlers how to parse, render, load and describe the page. Support data is from MDN and web.dev's Baseline as of September 2026. Body markup is in Semantic elements, styling in CSS, and the general accessibility rules in Accessibility.

Minimal valid document

index.html
<!doctype html>
<html lang="en-GB">
  <head>
    <meta charset="utf-8">
    <meta name="viewport"
      content="width=device-width, initial-scale=1">
    <title>Pricing – Acme</title>
  </head>
  <body>
    <h1>Pricing</h1>
  </body>
</html>
LineWhy it mattersIf you leave it out
<!doctype html>puts the parser in no-quirks (standards) mode; case-insensitivequirks mode: legacy line-height, percentage-height and table quirks
<html lang="en-GB">language for screen-reader voices, hyphenation (hyphens: auto), spell-check, font selection for CJK, translation prompts; WCAG 3.1.1 (level A)voices mispronounce, hyphens does nothing, audits fail
<meta charset="utf-8">fixes the encoding before any text is decoded; must sit entirely within the first 1024 bytesbrowser guesses (often windows-1252): mojibake like ’ for ’
<meta name="viewport">mobile browsers use the device width instead of a ~980 px virtual desktoppage renders zoomed out on phones; mobile breakpoints never match
<title>required by the spec; tab and history label, bookmark name, default search result title, first thing a screen reader announcesvalidators error; tab shows the URL

What you can omit: the <html>, <head> and <body> tags are all optional in the HTML syntax (the parser creates the elements anyway), but write them so lang has a home and the structure is obvious. Closing slashes on void elements (<meta … />) are allowed and ignored.

Parsing & rendering modes

ModeTriggered byEffect
No-quirks (standards)<!doctype html>modern CSS behavior; what you want
Limited-quirks ("almost standards")some legacy doctypes, e.g. XHTML 1.0 Transitional, HTML 4.01 Transitional with a system identifierstandards, except inline images in table cells get the old line-height gaps
Quirksno doctype, or an old/unknown public identifierNavigator 4 and IE5-era emulation: unitless lengths and hashless colors accepted, percentage heights and line heights computed differently, tables don't inherit font sizes, body fills the viewport
  • Check the mode from DevTools: document.compatMode is "CSS1Compat" (standards) or "BackCompat" (quirks).
  • Anything before the doctype (a BOM is fine; a comment or whitespace is fine; text is not) can push the page into quirks mode.
  • The HTML parser never fails: missing end tags, stray </p> and misnested tags are all repaired by fixed rules, which is why invalid markup "works" but may not build the tree you think. The repairs are covered in Semantic elements.

Head elements

Only these eight elements may appear in <head>:

ElementPurposeRules and gotchas
titlepage titleexactly one; unique per page; put the specific part first: Pricing – Acme
metametadata by charset, name, http-equiv or itempropvoid element; unknown names are ignored
linkrelationship to another resource (rel + href)rel is required; several rel values are also allowed in body (stylesheet, preload, preconnect, prefetch, modulepreload, dns-prefetch)
basebase URL and default target for every relative URLat most one, before any element that uses a URL; also rewrites href="#top" to point at the base URL, not the current page
styleinline CSSmedia, nonce (for CSP), blocking="render"; type="text/css" is redundant
scriptclassic scripts, modules, import maps, speculation rules, data blockssee Script loading
noscriptfallback when scripting is disabledinside head it may contain only link, style and meta
templateinert markup fragment for later cloning; declarative shadow DOMnot rendered; images in it don't load, scripts don't run

Anything else (a div, an img, stray text) makes the parser close the head early and start the body, so everything after it lands in <body>. A misplaced tracking pixel can push your meta and link tags out of the head.

Scripts never run in these sandboxed demos, so the <noscript> fallback shows and the <template> stays invisible:

noscript-template.html
<script>document.body.append("JavaScript ran.")</script>
<noscript>
  <p>Scripting is disabled in this frame, so the
    noscript fallback renders.</p>
</noscript>
<template id="row">
  <p>Parsed but inert: never shown until cloned.</p>
</template>
<p>Nothing from the template appears above.</p>
Result

Meta tags

TagValuesNotes
<meta charset="utf-8">utf-8 is the only valid value in HTMLfirst in head; within the first 1024 bytes
name="viewport"width=device-width, initial-scale=1see the options table below
name="description"1–2 sentence summaryoften used as the search snippet; Google rewrites it when it judges the page text a better match. No fixed length limit; snippets are truncated to fit the screen
name="robots"noindex, nofollow, none, nosnippet, max-snippet:50, max-image-preview:large, max-video-preview:-1, notranslate, noimageindex, unavailable_after:2027-01-01, indexifembeddeddefault is all; name="googlebot" targets Google only; Google no longer uses noarchive
name="theme-color"a CSS color; add media for light/darktints browser UI in Safari (macOS, iOS) and Chrome on Android and in installed PWAs; Firefox ignores it (not Baseline)
name="color-scheme"light, dark, light dark, only lighttells the browser before CSS loads which schemes the page supports, so the default canvas and form controls start dark: no white flash (Baseline widely available)
name="referrer"no-referrer, origin, strict-origin-when-cross-origin (browser default), same-origin, unsafe-url…page-wide referrer policy; per-link referrerpolicy overrides it
name="keywords"comma listignored by Google; don't bother
name="generator", name="author", name="application-name"free textharmless metadata
http-equiv="content-security-policy"a CSP stringworks, but frame-ancestors, report-uri and sandbox are ignored in a meta tag, and report-only policies can't be set this way; prefer the header
http-equiv="refresh""5" or "0; url=/new"timed reloads and redirects fail WCAG 2.2.1 and 3.2.5 unless instant; use an HTTP 301/308 for moves
http-equiv="content-type"text/html; charset=utf-8legacy spelling of meta charset; use one or the other, not both
http-equiv="x-ua-compatible"IE=edgeInternet Explorer only; delete it

Viewport options

KeyValuesAdvice
widthdevice-width or pixelsalways device-width
initial-scale0–101: start at 100 % zoom
minimum-scale, maximum-scale0–10don't cap zoom; WCAG 1.4.4 requires 200 % text resize
user-scalableyes / nonever no: it blocks low-vision users; iOS ignores it since iOS 10 anyway
viewport-fitauto, contain, covercover extends under a notch; then pad with env(safe-area-inset-*)
interactive-widgetresizes-visual (default), resizes-content, overlays-contenthow the on-screen keyboard resizes the layout; Chrome and Firefox on Android only
<!-- full-bleed app shell that sits under the notch -->
<meta name="viewport" content="width=device-width,
  initial-scale=1, viewport-fit=cover,
  interactive-widget=resizes-content">

The color-scheme meta (or the CSS property it mirrors) switches the browser's own colors: the canvas, form controls and scrollbars. This demo sets the property per box to show both:

color-scheme.html
<style>
  .pane { padding: 10px; margin-block-end: 8px;
    background: Canvas; color: CanvasText; }
  .light { color-scheme: light; }
  .dark { color-scheme: dark; }
</style>
<div class="pane light">light:
  <input value="Text"> <button>Button</button>
  <input type="checkbox" checked></div>
<div class="pane dark">dark:
  <input value="Text"> <button>Button</button>
  <input type="checkbox" checked></div>
Result
relUseKey attributes
stylesheetexternal CSS; render-blocking unless media doesn't matchmedia, crossorigin, integrity, disabled, blocking
iconfaviconsizes, type; iOS ignores it and uses apple-touch-icon
apple-touch-iconhome-screen icon on iOS/iPadOS (180×180 PNG)no sizes needed for one file
manifestweb app manifest (name, icons, display) for installable appscrossorigin="use-credentials" if it needs cookies; Firefox desktop ignores manifests
canonicalthe preferred URL for duplicate or parameterised pagesabsolute URL; one per page; self-referencing is fine
alternateanother version: language (hreflang), feed (type="application/rss+xml"), printhreflang="x-default" for the fallback page
preconnectopen DNS + TCP + TLS to an origin earlycrossorigin when the later request is CORS (fonts, fetch); only the few origins needed in the first seconds
dns-prefetchDNS lookup only; cheap fallback for less important originsBaseline 2025
preloadfetch a resource the parser would discover late, at high priority, for this pageas required; crossorigin for fonts and CORS fetches; type to skip unsupported formats; imagesrcset/imagesizes for responsive images
modulepreloadfetch, parse and compile a module (and optionally its graph) earlyBaseline 2023; as defaults to script
prefetchlow-priority fetch for a likely next navigationnot supported in Safari (behind a flag); the speculation rules API is Chromium's replacement
expectwith blocking="render", hold first paint until an element id is parsed (cross-document view transitions)Chromium and Safari 18.2+; not Baseline
me, author, license, searchidentity links (IndieWeb, Mastodon verification), author page, license, OpenSearch description

Resource-hint attributes

AttributeValuesWhy
asstyle, script, font, image, fetch, tracksets priority and the Accept header, and lets the later request reuse the preload; a wrong or missing as means a double download
crossorigin(empty) = anonymous, use-credentialsmust match the eventual request's CORS mode. Fonts are always fetched in CORS mode, so font preloads need crossorigin even on the same origin
fetchpriorityhigh, low, autonudges the browser's priority: high on the LCP image or its preload, low on below-the-fold carousels. Baseline 2024
typeMIME typebrowser skips preloads it can't use, e.g. type="font/woff2" or type="image/avif"
mediamedia querypreload only on matching viewports
blockingrendermakes a link, style or script explicitly render-blocking; Chromium and Safari, not Firefox

Script loading

MarkupFetchRunsOrderBlocks parser
<script src>immediately, pauses parsingas soon as fetcheddocument orderyes, fetch and run
<script> inlinenoneimmediately (after any pending stylesheets load)document orderyes, while it runs
<script defer src>in parallelafter parsing, before DOMContentLoadeddocument orderno
<script async src>in parallelas soon as fetched, interrupting the parserwhichever loads firstonly while running
<script type="module" src>in parallel, with its importslike deferdocument orderno
<script type="module" async>in parallellike asyncload orderonly while running
<script type="module"> inlineits importslike defer (after parsing)document orderno
<script nomodule>skipped by any browser that understands modules
HTML parsing      ████████░░░░░░░░░░█████████████████│ DOMContentLoaded
<script>                  ▒▒▒▒▒▒▒▒▓▓                 │   (parser waits for fetch + run)
<script defer>    ▒▒▒▒▒▒▒▒                           │▓▓ (runs after parsing, in order)
<script async>    ▒▒▒▒▒▒▒▒▓▓                          (runs as soon as fetched)
type="module"     ▒▒▒▒▒▒▒▒▒▒▒ (+ imports)            │▓▓ (deferred by default)
 
▒ fetch   ▓ execute   ░ parser blocked

Rules that follow from the table:

  • defer and async only apply to external classic scripts; on an inline classic script they do nothing. Modules are already deferred, so defer on a module does nothing.
  • An inline script that comes after a <link rel="stylesheet"> waits for that stylesheet (scripts can read styles), so a slow CSS file also delays the inline script and everything below it.
  • async suits independent scripts (analytics, ads). Anything that needs the DOM or another script should be defer or a module.
  • nomodule served the ES5 fallback for Internet Explorer 11. Every current browser supports modules; drop the fallback unless you have measured traffic that needs it.
  • document.write() from an async or defer script is ignored (with a console warning).

Import maps

Map bare specifiers (import "lit") to URLs without a bundler. Baseline 2023.

<script type="importmap">
{
  "imports": {
    "lit": "https://cdn.jsdelivr.net/npm/lit@3/+esm",
    "app/": "/js/app/"
  },
  "integrity": {
    "/js/app/main.js": "sha384-…"
  }
}
</script>
<script type="module">
  import { html } from "lit";
  import "app/main.js";
</script>
  • The map must come before the first module that uses a bare specifier. More than one import map per page works in Chromium and Safari 18.4+, but not yet in Firefox; keep to one.
  • scopes remaps within a URL prefix: "scopes": { "/legacy/": { "lit": "/vendor/lit2.js" } }.
  • The integrity key (subresource integrity for modules) is supported in all engines since 2025.

Other script types

typeWhat it is
omitted, text/javascriptclassic script (the attribute is redundant)
moduleES module: strict mode, deferred, CORS-fetched (cross-origin needs CORS headers)
importmapimport map JSON
speculationrulesJSON rules for prefetch/prerender of next pages; Chromium only
application/ld+json, any unknown typedata block: parsed as text, never executed; readable via textContent

Social & SEO metadata

Open Graph

Read by Facebook, LinkedIn, Slack, Discord, iMessage, Mastodon and most other unfurlers. Note property=, not name=.

PropertyRequired by the protocolNotes
og:titleyescan differ from <title> (drop the site suffix)
og:typeyeswebsite, article, profile, video.movie…
og:imageyesabsolute HTTPS URL; 1200×630 (1.91:1) is the common target; add og:image:alt, og:image:width, og:image:height
og:urlyescanonical URL; same as rel="canonical"
og:descriptionno1–2 sentences
og:site_name, og:localenoen_GB (underscore, not hyphen)
article:published_time, article:authornofor og:type="article"; ISO 8601 dates

X (Twitter) cards

X reads its own twitter: tags and, per its card documentation, falls back to the matching og: tags for title, description and image. So in practice you add only:

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@acme">

twitter:card is usually summary (small square thumbnail) or summary_large_image.

Structured data (JSON-LD)

Schema.org vocabulary in a data block. Google recommends JSON-LD over Microdata and RDFa; it feeds rich results (article dates, breadcrumbs, products, recipes, FAQs), not ranking directly.

RuleDetail
Describe what is visiblemarking up content that isn't on the page violates Google's guidelines
Types for articlesArticle, NewsArticle, BlogPosting; none of the properties is required, add what applies
DatesISO 8601 with a time zone: 2026-09-26T09:00:00+01:00
authora Person or Organization with name (just the name) and url
Test itGoogle's Rich Results Test and the Schema.org validator

Canonical and language alternates

<link rel="canonical"
  href="https://acme.example/en-gb/pricing/">
<link rel="alternate" hreflang="en-GB"
  href="https://acme.example/en-gb/pricing/">
<link rel="alternate" hreflang="de"
  href="https://acme.example/de/preise/">
<link rel="alternate" hreflang="x-default"
  href="https://acme.example/pricing/">

Every language version lists all versions, including itself; links must be reciprocal or Google ignores them.

Favicons in 2026

Andrey Sitnik's Evil Martians guide ("How to Favicon in 2026") reduces the pile of generated sizes to a few files:

FileSizeWho uses it
favicon.ico32×32older browsers, feed readers, anything requesting /favicon.ico directly
icon.svgvectorcurrent browsers; can switch colors with @media (prefers-color-scheme: dark) inside the SVG
apple-touch-icon.png180×180, opaque backgroundiOS/iPadOS home screen (iOS ignores rel="icon")
icon-192.png, icon-512.png, icon-mask.png192, 512, 512 maskableonly in the manifest, only for installable apps
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/manifest.webmanifest">

sizes="32x32" on the .ico stops Chrome preferring it over the SVG. Maskable icons need their content inside the central safe circle (about 80 % of the canvas).

Head order for performance

Order changes how early the browser discovers and prioritizes resources. Rick Viscomi's capo.js ranks head elements into groups; keep each group above the next:

#GroupElements
1Pragma directivesmeta charset, meta http-equiv, meta name="viewport", base
2Titletitle
3Critical resource hintslink rel="preconnect", link rel="preload" with fetchpriority="high"
4Async scriptsscript async src
5Import styles<style> containing @import
6Synchronous scriptsscript src without async/defer
7Synchronous styleslink rel="stylesheet", style
8Preloadslink rel="preload", link rel="modulepreload"
9Deferred scriptsscript defer src, modules
10Prefetch and prerender hintslink rel="prefetch", link rel="dns-prefetch", speculation rules
11Everything elsedescription, Open Graph, icons, manifest, JSON-LD
  • Render-blocking: link rel="stylesheet" (matching media), <style>, and parser-blocking <script> in the head. Everything else can go below them.
  • Critical CSS inline plus the full sheet as a normal link is the usual compromise for above-the-fold speed; don't hand-roll "async CSS" hacks unless you've measured a gain.
  • Third-party tags injected by a tag manager are discovered late; preconnect to their origin if they are critical, or better, don't make them critical.

Common mistakes

MistakeEffectFix
meta charset after a long <title> or inline scriptoutside the first 1024 bytes: encoding may be sniffed, page re-parsedmake it the first child of head
No lang, or lang="en" on a German pagewrong voice and hyphenation; WCAG 3.1.1 failureset it on html; override on elements with lang
user-scalable=no / maximum-scale=1blocks zoom for low-vision usersremove; fix the input zoom on iOS with a 16 px font size instead
Font preload without crossoriginfont downloaded twiceadd crossorigin
Preloading five images and three scriptsfights the real critical pathpreload only what the parser finds late
async on scripts that depend on each otherintermittent "X is not defined"defer or modules
Relative og:image or canonicalunfurlers and crawlers resolve it wrongly or not at allabsolute HTTPS URLs
noindex copied from staging to productionpage drops out of searchcheck robots meta in the release checklist
<base href> added for convenienceevery href="#section" link now leaves the pageavoid base; use root-relative URLs
@import inside CSS or <style>serial, blocking fetcheslink each file or bundle
Div, image or text in headhead closes early; later metadata lands in bodykeep only the eight head elements
Two <title> or two canonicalsthe first title wins in the tab; Google may ignore conflicting canonicals altogetherone each
meta http-equiv="refresh" for redirectsslow, disorienting, and a weaker signal to crawlers than a real redirectHTTP 301/308

Recipes

Boilerplate head

index.html
<!doctype html>
<html lang="en-GB">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
    content="width=device-width, initial-scale=1">
  <title>Pricing – Acme</title>
 
  <link rel="preconnect" href="https://cdn.acme.example"
    crossorigin>
  <link rel="stylesheet" href="/css/main.css">
  <script type="module" src="/js/main.js"></script>
 
  <meta name="description"
    content="Plans from free to enterprise, billed monthly.">
  <meta name="color-scheme" content="light dark">
  <link rel="canonical"
    href="https://acme.example/pricing/">
  <link rel="icon" href="/favicon.ico" sizes="32x32">
  <link rel="icon" href="/icon.svg" type="image/svg+xml">
  <link rel="apple-touch-icon"
    href="/apple-touch-icon.png">
  <link rel="manifest" href="/manifest.webmanifest">
</head>

SEO and social block

<meta name="description"
  content="How we cut our build from 9 to 2 minutes.">
<link rel="canonical"
  href="https://acme.example/blog/fast-builds/">
 
<meta property="og:type" content="article">
<meta property="og:title" content="Faster builds">
<meta property="og:description"
  content="How we cut our build from 9 to 2 minutes.">
<meta property="og:url"
  content="https://acme.example/blog/fast-builds/">
<meta property="og:image"
  content="https://acme.example/og/fast-builds.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt"
  content="Bar chart: build time falls from 9 to 2 min">
<meta property="og:site_name" content="Acme">
<meta property="og:locale" content="en_GB">
<meta property="article:published_time"
  content="2026-09-26T09:00:00+01:00">
 
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@acme">

Preloading a web font

<link rel="preload" href="/fonts/inter-var.woff2"
  as="font" type="font/woff2" crossorigin>
 
<style>
  @font-face {
    font-family: "Inter";
    src: url("/fonts/inter-var.woff2") format("woff2");
    font-weight: 100 900;
    font-display: swap;
  }
</style>

The href must match the @font-face URL exactly, or the browser downloads it twice. Preload only the one or two faces used above the fold (usually the regular text weight).

Dark-mode-aware theme color

<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#f4f1ea"
  media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#161616"
  media="(prefers-color-scheme: dark)">

Match each value to the page background at the top of the page, so the browser chrome blends into it. The first theme-color whose media matches wins.

JSON-LD Article

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Faster builds",
  "image": [
    "https://acme.example/og/fast-builds.png"
  ],
  "datePublished": "2026-09-26T09:00:00+01:00",
  "dateModified": "2026-09-27T14:20:00+01:00",
  "author": [{
    "@type": "Person",
    "name": "Sam Lee",
    "url": "https://acme.example/team/sam-lee/"
  }],
  "publisher": {
    "@type": "Organization",
    "name": "Acme",
    "url": "https://acme.example/"
  }
}
</script>

Page-specific render blocking for a view transition

<style>
  @view-transition { navigation: auto; }
</style>
<!-- hold first paint until #hero is parsed -->
<link rel="expect" href="#hero" blocking="render">

Chromium and Safari only; Firefox ignores both lines and simply navigates.

References