HTTP 200 with 17 characters of text: 14 sites measured for empty server HTML
What the browser shows and what the server sends are two different documents

I was building a tool that walks a site and records the state of each page, and while scanning the output table one row stood out: almost every column was zero or blank. The HTTP status on that row was 200. Nothing had failed.
Opening the same URL in a browser showed a completely normal page. So this was not "the fetch did not work". It was "the fetch worked and there is no text in what came back". To decide how to detect that condition and how to report it, on 2026-09-13 I fetched 14 Japanese and English sites under identical conditions and put the numbers side by side.
Start with the most extreme row
The front page of nhk.or.jp, Japan's public broadcaster:
https://www.nhk.or.jp/
HTTP 200
HTML bytes : 5,856
text characters : 17 (script/style/noscript removed, whitespace stripped)
whitespace words : 4
same-site links : 0
5,856 bytes of HTML carrying 17 characters of text, and not one link to its own domain. The page is assembled by JavaScript in the browser, so the document the server hands out first contains the frame and nothing else.
Push that row into a report unchanged and it reads as "a page with almost no content and no internal links" — which any reader will interpret as a problem with the site. The actual problem is that the measurement does not fit how the page is built.
Fourteen sites, one method
Conditions were held constant: one request each, redirects followed, a User-Agent that identifies itself honestly (NeverEmptySeoBot/1.0), and no JavaScript execution. "Text characters" is the body with script, style, noscript and template removed, tags stripped, and all whitespace deleted — a literal character count. "Words" is that same text split on whitespace.
| Site | Status | HTML bytes | Text chars | Words | Internal links | h1 |
|---|---|---|---|---|---|---|
| nhk.or.jp | 200 | 5,856 | 17 | 4 | 0 | 1 |
| jal.co.jp | 403 | 369 | 157 | 15 | 0 | 1 |
| note.com | 200 | 144,083 | 179 | 6 | 13 | 0 |
| zenn.dev | 200 | 237,522 | 645 | 16 | 20 | 0 |
| pixiv.net | 200 | 42,718 | 826 | 96 | 8 | 0 |
| notion.com | 200 | 240,969 | 2,115 | 250 | 71 | 1 |
| vercel.com | 200 | 527,998 | 3,113 | 279 | 75 | 1 |
| city.yokohama.lg.jp | 200 | 60,924 | 3,513 | 509 | 124 | 1 |
| mlit.go.jp | 200 | 63,033 | 4,450 | 308 | 122 | 0 |
| gov.uk | 200 | 85,460 | 4,614 | 798 | 54 | 1 |
| python.org | 200 | 52,610 | 5,435 | 1,001 | 63 | 5 |
| qiita.com | 200 | 309,785 | 6,039 | 311 | 184 | 0 |
| rakuten.co.jp | 200 | 382,598 | 7,954 | 883 | 300 | 0 |
| this site | 200 | 82,781 | 11,447 | 699 | 69 | 1 |
An incidental finding: 6 of the 14 sites (mlit.go.jp, rakuten.co.jp, note.com, zenn.dev, qiita.com, pixiv.net) had no h1 element at all in the HTML the server returned. That does not mean those pages have no h1 — it includes the case where the h1 is inserted by JavaScript. The same number carrying two meanings is the subject of this whole article, and here it is again in a different column.
The most useful thing in the table is that HTML size predicts nothing about text volume. note.com returns 144 KB of HTML containing 179 characters of text; vercel.com returns 528 KB containing 3,113. Any rule of the form "if the response is big there must be content in it" is dead on arrival.
A word-count threshold destroys every Japanese page it touches
The obvious detector is "flag the page if the text has very few words". It breaks the moment the page is not in a space-delimited language. Measure the same text in characters and in whitespace words and the ratio moves by a factor of four:
- gov.uk … 4,614 chars ÷ 798 words = 5.8 characters per word
- python.org … 5,435 ÷ 1,001 = 5.4
- mlit.go.jp … 4,450 ÷ 308 = 14.4
- qiita.com … 6,039 ÷ 311 = 19.4
- this site … 11,447 ÷ 699 = 16.4
Japanese does not put spaces between words, so a whitespace word count says nothing about how much text is on the page. Calibrate a threshold like "under 50 words means empty" on English pages and the front page of Japan's Ministry of Land, Infrastructure and Transport — 4,450 characters of text — gets judged on a score of 308. It cleared the threshold this time. A shorter Japanese page would not. Without a morphological analyser in the pipeline, word count is not a defensible content metric for CJK, and the failure is one-directional: it only ever accuses real pages of being empty.
Character count has its own bias in the other direction, of course — the same sentence is shorter in Japanese than in English. That is fine for this purpose. I am not comparing pages to each other; I am trying to separate "a few characters" from "a few thousand".
Two conditions instead of one. It still misses two of fourteen
So the detector does not rely on word count alone. It requires fewer than 50 words and zero links to the same site. A page whose text was never delivered usually has no navigation either, because the navigation is built by the same JavaScript, so the two conditions tend to fire together. Only when both hold does the tool report "the HTML the server sent was empty".
Against the 14 sites this flags two: nhk.or.jp and jal.co.jp. And it misses two:
- note.com — 179 characters, 6 words, but 13 internal links, so it is not flagged
- zenn.dev — 645 characters, 16 words, but 20 internal links, so it is not flagged
Both are obviously assembled client-side. The detector is biased toward under-reporting, deliberately. Telling someone their page is empty when it is not is worse than handing them a low number without a label, because the first one sends them to rewrite a page that is fine. But under-reporting silently is not acceptable either, so every row carries the note that JavaScript was not executed — enough for a reader looking at a suspiciously small number to work out why.
Raising the threshold catches note.com and zenn.dev. It also catches pages that are simply short, which are not defective. There is no correct place to put this line, so rather than moving it around I publish where it is and what it was computed from: the word count and the internal-link count are columns in the output, next to the verdict, so anyone can disagree with the verdict and keep the evidence.
Do not count 403 as empty
Row two of the table, jal.co.jp, returned HTTP 403: 369 bytes, 157 characters of text. Under the two conditions above it lands on the "empty" side, and it does not belong there.
403 is an answer. It says "not for this client". A JavaScript-rendered page says "you are being served the content, but not in a form you can read". Aggregate those together and both the diagnosis and the fix go wrong: the first is about headers, identity and network path, the second is about how you read the response.
Sites that open fine in a browser and refuse a plain request are not rare. On a different day (2026-08-26), testing a link checker against 15 well-known sites with a bot User-Agent, 6 of the 15 answered 403 — every one of them loading normally in a browser. Report those as broken links and you have just assigned a human the job of deleting working links. If a status code came back, it is information; carry it all the way to the output instead of flattening it.
The subject of the measurement changed without telling me
One more thing this session caught: the pixiv.net row. I requested https://www.pixiv.net/ and, after following redirects, what I actually read was https://www.pixiv.net/en/ — the English edition. The Japanese front page was never measured.
This class of mistake is invisible because the numbers are all internally consistent. The row just sits in the table saying "pixiv.net has 826 characters of text" and nothing about it looks wrong. The fix is mechanical: keep the final URL after redirects as its own column, next to the URL you asked for. Mine did, which is the only reason I noticed while assembling the table. Without that column I would have published it.
For the same reason the output carries requestedUrl and url separately, plus foundOn — which page's link led there. Numbers handed over on their own lose the record of what they were measured on, and that record is usually the part you need when a number looks strange.
Blank, zero, and not-measured are three different things
All of the above ended up being decisions about output format rather than about crawling. Three rules came out of it:
- Say what was not measured. Post-JavaScript content, Core Web Vitals, image file sizes and ranking positions cannot be obtained this way, so every row carries an explicit list of what was not looked at. A blank cell should never leave the reader guessing between "fine" and "never checked".
- Do not emit a score. A 0–100 overall score is somebody's weighting, not a measurement; two tools will score the same page differently and neither number can be verified. Ship the tag, the header and the status code the finding rests on instead.
- Return the pages you could not read as rows. A crawl that silently drops what it failed to fetch is indistinguishable from a crawl that found nothing wrong.
The third one mattered most here. The same reasoning splits images with no alt attribute from images with alt="" into two separate columns: an empty alt is the correct markup for a decorative image, not a defect, and merging them inflates the apparent amount of work to be done.
The crawler these rules went into
The detector and the output rules above are what Technical SEO Site Audit Crawler does: one row per page, stating what that page declares to a search engine (noindex, canonical, X-Robots-Tag, redirect chains) together with the evidence each claim came from. No overall score. URLs disallowed by robots.txt are not fetched and come back as a row saying they were skipped for that reason. Pages whose server HTML arrived empty carry the caveat described here rather than a blank.
The same "successful response, insufficient content" shape showed up from a completely different source, too: GitHub's search API reporting 7,705 matches and handing over 1,000 of them without an error. That one is about row counts and this one is about page text, and the handling turned out to be identical.
Written by GRAMSHIFT — an independent developer building Android apps and automation tools. The figures above come from calling the extraction stage of my own crawler against 14 sites, once each, on 2026-09-13; the measurement script and the organisation of this text were done with Claude Code, and the table was read back against the raw responses. These are single-fetch values from one connection at one moment, so the same sites will give different numbers on a different day.