How this was measured
On 2026-10-17 we requested the homepage of each of the same 78 hand-picked domains our other surveys use — news, commerce, SaaS, developer tooling, social, streaming, reference, SEO, education, finance, cloud, local and government — and parsed the opening tag and the <head> of whatever came back. Each request identified itself as a survey crawler and was attempted directly first, then through a local proxy.
62 of the 78 returned HTML we could parse (79.5%) using a 2 KB floor, and two of those were not homepages at all: khanacademy.org sent a 3 KB page titled "Client Challenge" and paypal.com sent 8 KB of "Security Challenge … Please wait while we perform security check". Their heads are the wall's, not the site's, so any verdict drawn from them would be fiction. That leaves 60 homepages for everything below.
A further seven are JavaScript shells — reddit.com (8 KB), x.com, pinterest.com, tumblr.com, youtube.com, twitch.tv and airbnb.com return under 500 characters of extractable body text. We kept them. The distinction matters: <html lang>, charset and viewport live in the head, which servers do emit, so those three are measurable; whether the declared language matches the words on the page is not, and every content-level claim here is restricted to the 53 sites that yield at least 500 characters of text.
Three parsing rules shaped the numbers, and two of them changed an answer. Inline script, style, template and noscript blocks are masked to equal-length whitespace before parsing — a string inside a bundle is not an element, but masking must preserve length because we measure byte offsets. Attribute regexes carry a left boundary ((?:^|\s)lang\s*=), because data-lang="x" ends in lang= and will otherwise be matched. Bot walls are excluded by title and body text, not by size — a 3 KB challenge page and a 944 KB single-page app look nothing alike, and only one of them is a homepage.
Finding 1: one site in sixty has no language, and it is the one that needs it most
59 of the 60 homepages declare a language on <html> (98.3%). The exception is twitch.tv, whose opening tag is <html class="tw-root--hover"> — no lang, no dir.
That would be a footnote except for what sits 100 bytes later. twitch.tv declares 28 rel="alternate" hreflang entries plus an x-default: bg, cs, da, de, el, es, es-mx, fi, fr, hu, it, ja, ko, nl, no, pl, pt, pt-br, ro, ru, sk, sv, th, tr, vi, zh-cn, zh-tw. It has told Google exactly which URL serves which language in 28 languages and has not said which language the current document is in. Screen readers fall back to the user's default voice; a bilingual visitor gets whatever their OS guessed.
Among the 59 that do declare one, 37 write a bare en and 20 write a region tag (33.9%): en-us 15, en-gb 3, plus nodejs.org at en-gb — British English on a US foundation's site, which is a choice someone made and which nobody notices until a date format appears. Zero values fail a basic BCP 47 shape check, and zero are empty.
Four sites declared a language other than English, and all four are an artefact of where we asked from, not defects: stripe.com returned zh-cn with Chinese navigation, youtube.com ja-jp with a Japanese footer, linkedin.com zh, bloomberg.com ja. Our exit IP is in Asia. We report them because a language audit run from the wrong continent will "find" mismatches that do not exist, and because they are the reason Finding 5 compares primary subtags rather than whole strings.
Finding 2: two sites name eleven and thirteen languages but not their own
29 of the 60 homepages declare alternate hreflang entries, a median of 13 each and a maximum of 89 (stripe.com, then shopify.com 85, mozilla.org 81, wise.com 46, pinterest.com 37, figma.com 34). 20 of the 29 include x-default (69.0%).
Two do not include themselves. atlassian.com opens with <html lang="en"> and then lists ja, fr, de, es, pt-BR, zh-Hans, ko, ru, pl, it, nl — eleven languages, no en, no x-default. kubernetes.io opens with <html lang="en"> and lists bn, zh-cn, fr, de, hi, id, it, ja, ko, fa, pl, pt-br, ru, es, uk, vi — thirteen, no en. Both were checked against the raw tags, and both match what our earlier hreflang survey found from a different angle and a different snapshot: the English page is the one URL in the cluster that never gets an annotation.
Worth stating how easy it is to manufacture this finding by mistake. Our first comparison asked whether the literal string lang appears in the hreflang set, which flags atlassian.com's lang="en" against an en-us entry thirty times over. Comparing primary subtags, only these two survive.
Finding 3: every charset is UTF-8, and three declare it too late to matter
59 of 60 declare a character encoding and all 59 say UTF-8 — not one latin-1, not one legacy encoding. 58 do it with <meta charset> and one (squareup.com) with the older http-equiv="Content-Type". Six write it twice and none of the six disagree with themselves. youtube.com is the only site where the string charset does not appear anywhere in its 944 KB; it relies on the HTTP header, which is legal and which this method cannot see.
The position is where the interesting part is. Google's documented rule is that it looks at the first 1,024 bytes when sniffing an encoding. 54 of the 59 declare inside the first 512 bytes and the median offset is 88 — exactly where the spec wants it. Three do not:
cnn.com declares <meta charset="utf-8"> at byte 309,464. We read the raw file to confirm it: the declaration is real, it is the first one in the document, and what precedes it is a single inline <script> holding a JSON configuration blob roughly 300 KB long, itself preceded by font preloads. For any consumer that honours the 1,024-byte window, this page has no in-document encoding declaration at all.
slack.com at byte 11,218, behind a <script type="text/human"> containing an 11 KB ASCII-art Slack logo. A comment for human readers, shipped to every visitor, pushing the encoding declaration out of the window it needs to be in.
shopify.com at byte 1,986, written as <meta charSet="utf-8"/> — React's camelCase, which HTML treats identically since attribute names are case-insensitive, sitting behind a hydration-monitoring script.
None of the three is broken in a browser: all serve a UTF-8 Content-Type header, which wins. The finding is narrower and still useful — the in-document declaration, the one thing a saved file or a mirrored copy carries with it, has been moved somewhere it cannot do its job.
Finding 4: four homepages forbid zooming, and one of them writes "no" as 0
58 of 60 declare a viewport; youtube.com and twitch.tv do not. Of those 58, 56 write width=device-width. The content string is remarkably uniform: 23 distinct values across 58 sites, and 23 sites use the exact canonical width=device-width, initial-scale=1 — one in six homepages here is byte-identical on this line.
The exceptions are more interesting than the majority. wikipedia.org and wikimedia.org omit width entirely, writing only initial-scale=1,user-scalable=yes; an explicit initial scale is enough to make the layout viewport match the device, and the Wikimedia markup predates the convention. github.com, linkedin.com and netlify.com write width=device-width with no scale at all. bloomberg.com and tumblr.com ship two viewport tags each — Bloomberg's first is width=device-width and its second is width=device-width, initial-scale=1, viewport-fit=cover, so counting only the first would have reported a missing scale that is not missing.
Then there is zoom. Four homepages disable it outright (6.9%):
x.com writes width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover. netflix.com writes maximum-scale=1.0 and minimum-scale=1.0. screamingfrog.co.uk and vercel.com both write maximum-scale=1. target.com writes maximum-scale=2, which still permits doubling but caps it.
To count those four we had to fix our own parser. user-scalable has a documented keyword form (no) and an undocumented numeric form (0), and x.com uses the numeric one. Our first pass matched only no and reported zero sites disabling zoom — a completely clean result that was wrong. The maximum-scale check is what caught it. Three sites go the other way and affirmatively permit zoom (forbes.com, wikipedia.org, wikimedia.org with user-scalable=yes).
The rest of the vocabulary is thin: viewport-fit=cover on 6 sites, interactive-widget=resizes-content on 2 (walmart.com, target.com), and shrink-to-fit=no — a Safari-private keyword that has been obsolete for years — on 4: cnn.com, kubernetes.io, squareup.com, cdc.gov.
Finding 5: one in four still ships an Internet Explorer instruction
17 of the 60 homepages (28.3%) send <meta http-equiv="X-UA-Compatible">, a tag whose only purpose is to select a rendering engine in Internet Explorer 8 through 11. IE reached end of support in June 2022 and was removed from Windows by update in 2023. The values are a museum: IE=edge on cnn.com, target.com, netflix.com, moz.com, wise.com and others; IE=edge,chrome=1 on slack.com, asana.com, trello.com, linkedin.com, who.int — the chrome=1 half invoking a Chrome Frame plugin that was discontinued in 2014; IE=9 on spotify.com, which asks for a specific 2011 engine; and IE=Edge,chrome=IE7 on coursera.org, which contains two discontinued products in one attribute.
Nothing breaks. It is dead weight in the head of a quarter of the web, and it is the clearest available illustration of how long a tag survives after the reason for it is gone.
Finding 6: theme-color is on 26 sites, and one of them forgot the colour
26 of 60 declare theme-color, five with prefers-color-scheme media variants — x.com pairs #FFFFFF and #000000, vercel.com pairs #FAFAFA and #000. Five sites declare color-scheme (github.com and x.com at light dark, yoast.com at only light).
nodejs.org ships two theme-color tags with media queries and no content attribute on either: <meta name="theme-color" media="(prefers-color-scheme: light)"/> and the same for dark. We printed both tags to be sure. The element is valid HTML, no validator objects, and it instructs the browser to tint its UI with nothing. It is the same class of defect as an empty href: complete markup, no effect.
Eight sites set format-detection to stop iOS auto-linking phone numbers (all telephone=no, with stripe.com adding email=no) and five carry the apple-mobile-web-app-* trio for home-screen installation.
Finally, scale: 1,312 <meta> tags across the 60 heads, a median of 20.5 each, from 2 on reddit.com to 53 on github.com. A quarter of the way down the list the tags stop being about the document and start being about analytics, consent and app links.
What to ship
Put lang on <html> before you do anything else with language. One site in sixty here annotates 28 languages and cannot say what the current page is in. If you serve more than one language, add dir too — 14 sites in this sample do, all ltr.
Declare charset in the first 1,024 bytes, and keep it there. The median here is byte 88. The three failures are all self-inflicted: a 300 KB inline config script, an 11 KB ASCII-art comment, and a monitoring snippet. Anything large that runs before <meta charset> is a candidate for moving after it.
Write width=device-width, initial-scale=1 and nothing else. It is what 23 of 58 sites use verbatim. Do not add maximum-scale or user-scalable — four homepages here block zoom entirely and the World Wide Web Consortium's accessibility guidance treats that as a failure. If you must cap it, note that some sites write user-scalable=0 rather than no, so grep for the digit too.
Drop X-UA-Compatible and shrink-to-fit. 17 sites and 4 sites respectively, both targeting software that no longer exists.
If you write theme-color, give it a value, and prefer the media-variant pair. Two empty tags on nodejs.org are the cautionary example.
When you annotate hreflang, annotate your own page too. Two of 29 sites here list eleven and thirteen languages and omit the one they are serving.
To assemble these tags from one input so they cannot drift apart, the meta tag generator emits title, description, canonical, robots and icon tags together, and the hreflang generator builds a self-referencing language cluster with x-default included.
Limitations
Homepages only, 60 of them, one snapshot. Everything was fetched on 2026-10-17 with a crawler identity. WAFs answer differently per client, and an article page under a language switcher behaves differently from the root.
Server-rendered HTML only, no response headers. This is the largest gap in the method. youtube.com has no charset in its 944 KB and that is almost certainly a header declaration, which we did not capture — the same blind spot our resource-hint survey had to close by reading Link headers. Every "missing" here means "missing from the HTML", and for five sites the real answer may be in a header.
Seven of the 60 are JavaScript shells. Their heads are real, so lang/charset/viewport stand, but nothing about their content does. Content-level claims cover 53 sites.
Localisation. Our exit IP is in Asia and four sites answered with a regional variant. Language-mismatch detection is therefore unreliable at the margins, and we report no mismatch findings at all rather than four artefacts.
We counted markup, not behaviour. Whether a browser honours the second of two viewport tags, whether a blocked zoom fails an audit, and whether a late charset ever mis-decoded a page for anyone, are outside this method. A "defect" here means the declared intent differs from the common case, not that a user was harmed.
Reproduce it
The script and the shared domain list ship with this site. node scripts/survey-docmeta.mjs re-parses every number from the stored homepage HTML without touching the network, so a parsing fix can be applied and re-run months after the fetch; --evidence=lang, =charset, =viewport, =blocked, =hreflang and =theme print the original tag behind every verdict. It reads the most recent snapshot in .workbuddy/tmp by default, which today is the 2026-10-17 fetch; pass --raw=<file> to pin a specific one. That is what confirmed cnn.com's 309 KB offset against the raw file, caught x.com's numeric user-scalable=0, exposed nodejs.org's two valueless theme-color tags, and killed two findings that were our own parser's fault — the og:locale separator and the literal-string hreflang comparison. Our hreflang, favicon, indexing directive and image surveys run the same domain list from other angles.