<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: tong zhang</title>
    <description>The latest articles on DEV Community by tong zhang (@tong_zhang_142dabd4f2b865).</description>
    <link>https://dev.to/tong_zhang_142dabd4f2b865</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4199629%2F427f0b6e-bf0d-4f8d-97c4-5d26ca7267fc.png</url>
      <title>DEV Community: tong zhang</title>
      <link>https://dev.to/tong_zhang_142dabd4f2b865</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tong_zhang_142dabd4f2b865"/>
    <language>en</language>
    <item>
      <title>Chrome got zstd for the HTML and gzip for the CSS. Brotli was 279 KB smaller.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Fri, 13 Nov 2026 02:18:26 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/chrome-got-zstd-for-the-html-and-gzip-for-the-css-brotli-was-279-kb-smaller-52f8</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/chrome-got-zstd-for-the-html-and-gzip-for-the-css-brotli-was-279-kb-smaller-52f8</guid>
      <description>&lt;p&gt;The page is 7.7 KB. The stylesheet it loads is 312 KB. Both came from the same host, one second apart, with the same request headers.&lt;/p&gt;

&lt;p&gt;The HTML arrived as &lt;code&gt;zstd&lt;/code&gt;. The CSS arrived as &lt;code&gt;gzip&lt;/code&gt; — even though the same &lt;code&gt;Accept-Encoding&lt;/code&gt; header listed brotli, and the same edge was willing to send brotli for the HTML. Asking for brotli alone on that stylesheet returns 33 KB.&lt;/p&gt;

&lt;p&gt;So a browser that offers every encoding the server supports downloads 312 KB where 33 KB was available. That is one file, on one of 114 sites I run. I spent an afternoon measuring where else this happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually measured
&lt;/h2&gt;

&lt;p&gt;Two datasets, both taken today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;684 HTML responses&lt;/strong&gt; — 114 sites × 6 &lt;code&gt;Accept-Encoding&lt;/code&gt; variants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;492 assets&lt;/strong&gt; — the same-origin CSS and JavaScript referenced by those 114 homepages, each fetched three ways (browser header, brotli only, identity).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole method is one idea: read the bytes, do not trust the label. No decompression library, no &lt;code&gt;requests&lt;/code&gt; magic, no CDN analytics. &lt;code&gt;len(body)&lt;/code&gt; after reading the raw stream is the number the user pays for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="no"&gt;CTX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;CTX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;span class="no"&gt;CTX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verify_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CERT_NONE&lt;/span&gt;

&lt;span class="no"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "&lt;/span&gt;
      &lt;span class="s2"&gt;"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wire_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accept_encoding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"User-Agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"Accept"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"*/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"Accept-Encoding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;accept_encoding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;CTX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"encoding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"content-encoding"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"wire"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"vary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"vary"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cf-cache-status"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details matter more than they look. &lt;code&gt;read()&lt;/code&gt; with no argument, because a truncated read makes big files look identical. No brotli library installed, because I do not need to decode anything. And a browser User-Agent, because some of these hosts behave differently for unknown clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HTML got zstd, all 114 of them
&lt;/h2&gt;

&lt;p&gt;Chrome's real header is &lt;code&gt;gzip, deflate, br, zstd&lt;/code&gt;. Fonts and assets offered the same set. For HTML, all 114 sites answered &lt;code&gt;zstd&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;Accept-Encoding&lt;/code&gt; sent&lt;/th&gt;
&lt;th&gt;Encoding returned&lt;/th&gt;
&lt;th&gt;Sites&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gzip, deflate, br, zstd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;zstd&lt;/td&gt;
&lt;td&gt;114 / 114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gzip, deflate, br&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;br&lt;/td&gt;
&lt;td&gt;114 / 114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;br&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;br&lt;/td&gt;
&lt;td&gt;114 / 114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gzip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gzip&lt;/td&gt;
&lt;td&gt;114 / 114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;identity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none (raw)&lt;/td&gt;
&lt;td&gt;114 / 114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;br;q=0, gzip;q=0, deflate;q=0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none (raw)&lt;/td&gt;
&lt;td&gt;114 / 114&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things to like here. &lt;code&gt;identity&lt;/code&gt; is honoured everywhere, so the servers are not compression bullies. And refusing every coding with &lt;code&gt;q=0&lt;/code&gt; returns a plain 200 with raw bytes rather than a 406, which is the forgiving behaviour you want from a CDN.&lt;/p&gt;

&lt;p&gt;The part I did not expect: zstd loses on size. I fetched every homepage three times and compared the wire bytes per encoding.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Encoding&lt;/th&gt;
&lt;th&gt;Median bytes&lt;/th&gt;
&lt;th&gt;Median share of raw&lt;/th&gt;
&lt;th&gt;Total across 114 pages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;raw (&lt;code&gt;identity&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;75,220&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;9.70 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gzip&lt;/td&gt;
&lt;td&gt;10,961&lt;/td&gt;
&lt;td&gt;16.4%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;br&lt;/td&gt;
&lt;td&gt;10,855&lt;/td&gt;
&lt;td&gt;15.6%&lt;/td&gt;
&lt;td&gt;1.30 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zstd&lt;/td&gt;
&lt;td&gt;11,710&lt;/td&gt;
&lt;td&gt;16.4%&lt;/td&gt;
&lt;td&gt;1.37 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Brotli beat zstd on 113 of 114 homepages. Median difference: 566 bytes, 6.3%. Across all 114 pages, zstd cost 72,493 bytes more than brotli would have — 636 bytes per page. Small, but it is a real tax paid for choosing the newer algorithm: at this compression level, the newest codec is the loosest one.&lt;/p&gt;

&lt;p&gt;The exception is worth keeping: one homepage ships a 666,877-byte Next.js document with a lot of repeated JSON, and there zstd won by 5.8% (45,740 vs 48,554 bytes). Repetition-heavy payloads are exactly where zstd's window pays off. Brotli is not universally better; it is better on prose-shaped HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  The assets got gzip, 480 out of 492
&lt;/h2&gt;

&lt;p&gt;This is where the numbers stop being a rounding error.&lt;/p&gt;

&lt;p&gt;I pulled every same-origin stylesheet and script out of those 114 homepages, deduplicated by URL, and fetched each one three times. 480 of 492 came back &lt;code&gt;gzip&lt;/code&gt; when the client offered brotli and zstd. Twelve came back &lt;code&gt;zstd&lt;/code&gt;. Zero came back brotli.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset type&lt;/th&gt;
&lt;th&gt;Files&lt;/th&gt;
&lt;th&gt;Raw&lt;/th&gt;
&lt;th&gt;Delivered to Chrome&lt;/th&gt;
&lt;th&gt;With brotli&lt;/th&gt;
&lt;th&gt;Saved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CSS&lt;/td&gt;
&lt;td&gt;156&lt;/td&gt;
&lt;td&gt;15.19 MB&lt;/td&gt;
&lt;td&gt;3.58 MB&lt;/td&gt;
&lt;td&gt;2.00 MB&lt;/td&gt;
&lt;td&gt;1.58 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;335&lt;/td&gt;
&lt;td&gt;45.07 MB&lt;/td&gt;
&lt;td&gt;13.16 MB&lt;/td&gt;
&lt;td&gt;13.10 MB&lt;/td&gt;
&lt;td&gt;0.06 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;492&lt;/td&gt;
&lt;td&gt;60.31 MB&lt;/td&gt;
&lt;td&gt;16.75 MB&lt;/td&gt;
&lt;td&gt;15.11 MB&lt;/td&gt;
&lt;td&gt;1.64 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nine point eight percent of the bytes every visitor downloads, available for free, not taken.&lt;/p&gt;

&lt;p&gt;The distribution is lopsided. Eight CSS files account for 80% of the entire saving, and almost all of it sits in one filename that recurs across the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s -D - -o /dev/null -H 'Accept-Encoding: gzip, deflate, br, zstd' \
    https://spacexstockprediction.com/static/css/css2.css | grep -i content-encoding
content-encoding: gzip        # 312,302 bytes

$ curl -s -D - -o /dev/null -H 'Accept-Encoding: br' \
    https://spacexstockprediction.com/static/css/css2.css | grep -i content-encoding
content-encoding: br          # 33,328 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same URL, same cache entry, same minute. The difference is 279 KB per page load on that host, and the same pattern repeats on 15 sites in the cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is inside a 1.09 MB stylesheet
&lt;/h2&gt;

&lt;p&gt;I fetched it with &lt;code&gt;identity&lt;/code&gt; to see what was being compressed so poorly. It is 1,094,451 characters of &lt;code&gt;@font-face&lt;/code&gt; rules for Noto Sans SC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@font-face blocks : 1,010
unicode-range     : 1,010
url() references  : 1,010   (all self-hosted /static/font/*.woff2)
font-display      : swap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the Google Fonts CJK slicing pattern, copied in-house: a hundred-odd unicode-range subsets per weight, several weights. The page that loads it, &lt;a href="https://spacexstockprediction.com/spacex-investment-basics/" rel="noopener noreferrer"&gt;a SpaceX explainer on the worst offender&lt;/a&gt;, contains 223 Han characters. The browser will only fetch the few subsets it needs for those 223 characters, so the fonts themselves are not the problem — the problem is that 1.09 MB of rules has to cross the wire and be parsed before the browser knows which subsets exist.&lt;/p&gt;

&lt;p&gt;And this cluster has a language mismatch that makes it worse: there are &lt;a href="https://coinatlaslab.com/guide/ethereum-staking-guide-chinese/" rel="noopener noreferrer"&gt;Chinese-language guides served from an English-language template&lt;/a&gt;, which is exactly the setup that ends up shipping a full CJK font manifest to every visitor.&lt;/p&gt;

&lt;p&gt;Cheap fix: that file does not change between deploys. Compress it once with brotli at build time, serve the &lt;code&gt;.br&lt;/code&gt; variant with &lt;code&gt;Content-Encoding: br&lt;/code&gt; and &lt;code&gt;Vary: Accept-Encoding&lt;/code&gt;. 1.09 MB becomes 33 KB for every browser released this decade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where brotli does not help at all
&lt;/h2&gt;

&lt;p&gt;I expected the JavaScript column to look like the CSS column. It does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;brotli beat gzip on 138 of 335 JavaScript files.&lt;/li&gt;
&lt;li&gt;On 196 of 335 it was &lt;strong&gt;larger&lt;/strong&gt;. Median difference: −0.4%.&lt;/li&gt;
&lt;li&gt;Total: 13.16 MB → 13.10 MB. Half a percent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever level this edge compresses at, it is not the level where brotli's context modelling starts to matter on minified JS. So the honest summary is not "turn on brotli". It is "turn on brotli where the file is repetitive text, and measure the rest" — CSS yes, minified JS no, HTML marginally.&lt;/p&gt;

&lt;p&gt;If you want a second opinion on any of this, pick a page from the same template, for example &lt;a href="https://eventforecasthub.com/markets/bitcoin-120000-price-2026/" rel="noopener noreferrer"&gt;a market page from the same cluster&lt;/a&gt;, and compare its &lt;code&gt;content-encoding&lt;/code&gt; against &lt;code&gt;curl -H 'Accept-Encoding: br'&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The negotiation table that explains it
&lt;/h2&gt;

&lt;p&gt;Probing one cached CSS file with different header combinations, same URL, all &lt;code&gt;cf-cache-status: HIT&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;Returned&lt;/th&gt;
&lt;th&gt;Bytes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gzip, deflate, br, zstd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gzip&lt;/td&gt;
&lt;td&gt;250,677&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;br, gzip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gzip&lt;/td&gt;
&lt;td&gt;250,677&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gzip, br&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gzip&lt;/td&gt;
&lt;td&gt;250,677&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gzip, deflate, br&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gzip&lt;/td&gt;
&lt;td&gt;250,677&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zstd, br&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;zstd&lt;/td&gt;
&lt;td&gt;35,999&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;br&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;br&lt;/td&gt;
&lt;td&gt;33,163&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It is not header order. Any header that mentions &lt;code&gt;gzip&lt;/code&gt; gets gzip on the asset path; drop gzip and the same file is served as zstd; ask for brotli alone and you get brotli. Meanwhile the HTML path, on the same zone, prefers zstd and never falls back to gzip while brotli is on the table.&lt;/p&gt;

&lt;p&gt;I cannot see the CDN configuration from outside, so I am not going to claim a cause. What I can say is that the asset path and the document path negotiate differently, in the same zone, in the same second, and that the asset path's choice is the expensive one for CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two hygiene findings, one good, one not
&lt;/h2&gt;

&lt;p&gt;All 492 assets vary their bytes by &lt;code&gt;Accept-Encoding&lt;/code&gt;, and &lt;strong&gt;none of them send &lt;code&gt;Vary&lt;/code&gt;&lt;/strong&gt;. All 114 HTML responses do. If anything between the edge and the client caches by URL alone — a corporate proxy, a service worker with a careless cache key, a reverse proxy you forgot about — it can hand brotli bytes to a client that never advertised brotli, and that client will render garbage. The edge's own cache key handles it; nothing downstream is told to.&lt;/p&gt;

&lt;p&gt;The other one is smaller but universal: &lt;strong&gt;114 of 114 responses have no &lt;code&gt;Content-Length&lt;/code&gt;&lt;/strong&gt; and use &lt;code&gt;Transfer-Encoding: chunked&lt;/code&gt;. Every homepage in the cluster streams without a declared size. That is not a bug, but it removes the browser's ability to show progress or reserve space, and it makes byte-count auditing harder than it should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am doing about it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Precompressing the CJK stylesheets at build time and serving brotli variants with &lt;code&gt;Vary: Accept-Encoding&lt;/code&gt;. That is 1.58 MB of the 1.64 MB, and it needs no configuration change on the edge.&lt;/li&gt;
&lt;li&gt;Re-checking per type instead of globally. CSS gains 44% of its bytes; minified JS gains 0.5%.&lt;/li&gt;
&lt;li&gt;Adding wire-byte assertions to the deploy check, comparing &lt;code&gt;len(body)&lt;/code&gt; for &lt;code&gt;gzip&lt;/code&gt; versus &lt;code&gt;br&lt;/code&gt; on the three largest text files. A regression here is invisible in every dashboard I have.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The uncomfortable part is that this cluster has been measuring performance for weeks — TTFB, &lt;code&gt;cf-cache-status&lt;/code&gt;, &lt;code&gt;304&lt;/code&gt; rates, image weight — and every one of those measurements looked fine, because none of them asked what encoding the bytes arrived in. The page was fast. It was also 279 KB heavier than it needed to be, on a file that has not changed since the last deploy.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>http</category>
      <category>css</category>
    </item>
    <item>
      <title>21 of our 114 sites answer every missing URL with the homepage. Search Console calls that indexed.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Wed, 11 Nov 2026 02:03:53 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/21-of-our-114-sites-answer-every-missing-url-with-the-homepage-search-console-calls-that-indexed-4g9a</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/21-of-our-114-sites-answer-every-missing-url-with-the-homepage-search-console-calls-that-indexed-4g9a</guid>
      <description>&lt;p&gt;I asked 114 of my own sites for a page that does not exist. Sixty-four times, the server said yes.&lt;/p&gt;

&lt;p&gt;Not "yes, here's an error page." Yes, here is a 200, here are 53,861 bytes, and here is the homepage — the identical bytes, down to the SHA-256 hash. Same title tag. Same &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; count (zero). Same everything.&lt;/p&gt;

&lt;p&gt;Then I pulled Search Console's page-indexing export and found 133 URLs marked &lt;code&gt;Submitted and indexed&lt;/code&gt; across the same hosts, and I started to understand how a site can look healthy on every dashboard and rank for nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "doesn't exist" means when you're the one asking
&lt;/h2&gt;

&lt;p&gt;The cluster is 114 sites I run. Mostly small static or edge-rendered content sites, all behind Cloudflare, all with a sitemap, all with Search Console properties. The kind of setup where you trust the status code because you never had a reason not to.&lt;/p&gt;

&lt;p&gt;A missing URL in HTTP has one correct answer: 404. Or 410 if you're being precise about intent. What it must not be is 200, because 200 is a promise. It tells every crawler, every cache, every link checker, and every downstream consumer that the resource is there.&lt;/p&gt;

&lt;p&gt;So I generated paths that could not possibly exist — a random hex token, three shapes per site — and asked each host for them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;probe_paths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"https://{host}/this-page-does-not-exist-{tok}/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"https://{host}/blog/missing-post-{tok}-9f2c/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"https://{host}/{tok}/nested/gone.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three hundred forty-two requests total. No retries, no bot user agents, plain Chrome UA, no &lt;code&gt;Accept-Encoding&lt;/code&gt; header (so I could read the bodies without a brotli decompressor).&lt;/p&gt;

&lt;h2&gt;
  
  
  64 of 342 came back 200
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;276&lt;/td&gt;
&lt;td&gt;80.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;18.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS handshake failure&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eighty percent behaved. That is the number that made me relax for about ten seconds, until I grouped by host instead of by request.&lt;/p&gt;

&lt;h2&gt;
  
  
  21 sites, one behaviour: the homepage, byte for byte
&lt;/h2&gt;

&lt;p&gt;Twenty-one of the 114 hosts answered &lt;em&gt;every&lt;/em&gt; nonexistent path with a 200. Not with a rendered error template — with their own front page.&lt;/p&gt;

&lt;p&gt;The giveaway was the byte count. Compare the body length of a fake path against the body length of &lt;code&gt;/&lt;/code&gt; on the same host, and the two numbers match exactly. Match exactly, on a page that includes a timestamped market table and a rotating ticker, is not a coincidence. It is a cache hit or a rewrite.&lt;/p&gt;

&lt;p&gt;I hashed them to be sure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="no"&gt;CTX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;CTX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;span class="no"&gt;CTX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verify_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CERT_NONE&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"User-Agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "&lt;/span&gt;
                      &lt;span class="s2"&gt;"AppleWebKit/537.36 (KHTML, like Gecko) "&lt;/span&gt;
                      &lt;span class="s2"&gt;"Chrome/131.0.0.0 Safari/537.36"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"Accept"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"text/html,*/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;CTX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;identical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"https://{host}/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"https://{host}/zz-{uuid.uuid4().hex[:8]}/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven hosts tested this way, seven identical:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Homepage bytes&lt;/th&gt;
&lt;th&gt;Fake-path bytes&lt;/th&gt;
&lt;th&gt;SHA-256 match&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nextgemcrypto.com&lt;/td&gt;
&lt;td&gt;53,861&lt;/td&gt;
&lt;td&gt;53,861&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spacexstockanalysis.com&lt;/td&gt;
&lt;td&gt;8,749&lt;/td&gt;
&lt;td&gt;8,749&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openaistocknews.com&lt;/td&gt;
&lt;td&gt;10,528&lt;/td&gt;
&lt;td&gt;10,528&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openaipreipo.com&lt;/td&gt;
&lt;td&gt;11,745&lt;/td&gt;
&lt;td&gt;11,745&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openaiipostock.com&lt;/td&gt;
&lt;td&gt;10,060&lt;/td&gt;
&lt;td&gt;10,060&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;howtobuyspacexstock.com&lt;/td&gt;
&lt;td&gt;8,090&lt;/td&gt;
&lt;td&gt;8,090&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bullrunalpha.com&lt;/td&gt;
&lt;td&gt;21,053&lt;/td&gt;
&lt;td&gt;21,053&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is the part that matters: these are not broken sites. On the same hosts, real URLs render real documents. &lt;code&gt;bullrunalpha.com&lt;/code&gt; is a 116-page site, and a sampled article there ships 25,023 bytes of distinct markup with its own title — &lt;a href="https://bullrunalpha.com/coin/bittensor-price-prediction-2026/" rel="noopener noreferrer"&gt;a coin page that does return its own document&lt;/a&gt;, which is exactly what makes the fallback so easy to miss. &lt;code&gt;spacexstockinsights.com&lt;/code&gt; behaves the same way: its &lt;a href="https://spacexstockinsights.com/secondary-market-pricing/" rel="noopener noreferrer"&gt;secondary-market pricing page&lt;/a&gt; returns 15,040 bytes against a 13,298-byte homepage, close enough in size to look right in a log and far enough apart to be a genuinely different page.&lt;/p&gt;

&lt;p&gt;The bug is not "the site is down." The bug is "any unknown path silently becomes the front page."&lt;/p&gt;

&lt;h2&gt;
  
  
  It isn't cloaking
&lt;/h2&gt;

&lt;p&gt;Cloaking would be a different, more serious finding: a different response for Googlebot than for a browser. I tested for it, because 21 sites sharing an exact behaviour smells like a shared template, and shared templates sometimes carry shared hacks. The matrix ran against the 19 hosts where all three probe requests completed; two others dropped a TLS handshake mid-run, and I left them out rather than retry my way into a different sample.&lt;/p&gt;

&lt;p&gt;Six request variants against all 19 hosts I deep-tested:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;soft-404 group&lt;/th&gt;
&lt;th&gt;control group&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome UA, trailing slash&lt;/td&gt;
&lt;td&gt;19 × 200&lt;/td&gt;
&lt;td&gt;10 × 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Googlebot UA&lt;/td&gt;
&lt;td&gt;19 × 200&lt;/td&gt;
&lt;td&gt;10 × 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bingbot UA&lt;/td&gt;
&lt;td&gt;19 × 200&lt;/td&gt;
&lt;td&gt;10 × 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome UA, no trailing slash&lt;/td&gt;
&lt;td&gt;19 × 200&lt;/td&gt;
&lt;td&gt;10 × 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome UA, &lt;code&gt;.html&lt;/code&gt; suffix&lt;/td&gt;
&lt;td&gt;19 × 200&lt;/td&gt;
&lt;td&gt;10 × 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome UA, four segments deep&lt;/td&gt;
&lt;td&gt;19 × 200&lt;/td&gt;
&lt;td&gt;10 × 404&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The control group is ten hosts drawn at random from the 92 that returned clean 404s. Every variant, every host, both directions. No UA sniffing, no cloaking, no exceptions.&lt;/p&gt;

&lt;p&gt;That uniformity is worse than cloaking in one specific way: a cloaking hack at least suggests someone was paying attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fallback survives every URL shape
&lt;/h2&gt;

&lt;p&gt;The trailing-slash and &lt;code&gt;.html&lt;/code&gt; rows above are the ones I expected to break. Static hosts usually treat &lt;code&gt;/missing&lt;/code&gt; and &lt;code&gt;/missing/&lt;/code&gt; as different keys, and a rewrite that matches one often misses the other.&lt;/p&gt;

&lt;p&gt;Not here. Whatever the mechanism, it matches at the routing layer, not the filesystem layer, and it matches greedily. Four-segment paths. Paths ending in &lt;code&gt;.html&lt;/code&gt;. Paths with a hex token where a slug should be. All 200, all the homepage.&lt;/p&gt;

&lt;p&gt;The one exception in the whole set is instructive. &lt;code&gt;coinwikipro.com&lt;/code&gt; returns 200 for missing paths too, but it returns a 10,269-byte page titled &lt;code&gt;404 · 页面未找到&lt;/code&gt; — someone did wire up an error page, and then served it with a 200 status and the homepage's &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; and body underneath. It knows the page is missing. It tells you in the title. It just tells the HTTP layer something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I checked the sitemaps
&lt;/h2&gt;

&lt;p&gt;A 200 on a made-up URL is embarrassing. A 200 on a URL you &lt;em&gt;declared in your sitemap&lt;/em&gt; is expensive, because that URL was supposed to be a real page.&lt;/p&gt;

&lt;p&gt;So for all 19 soft-404 hosts and the 10 controls, I pulled robots.txt, followed the &lt;code&gt;Sitemap:&lt;/code&gt; directive, expanded one level of sitemap index, and sampled six declared page URLs per host.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Sampled URLs&lt;/th&gt;
&lt;th&gt;Returned homepage bytes&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;soft-404 hosts&lt;/td&gt;
&lt;td&gt;114&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;53.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;control hosts&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fifty-three percent of the URLs these sites declare as content are serving the front page. The control group — same cluster, same templates, same Cloudflare account — sits at 3.7%.&lt;/p&gt;

&lt;p&gt;Five hosts scored six out of six: &lt;code&gt;nextgemcrypto.com&lt;/code&gt;, &lt;code&gt;openaiipostock.com&lt;/code&gt;, &lt;code&gt;openaipreipo.com&lt;/code&gt;, &lt;code&gt;openaistocknews.com&lt;/code&gt;, &lt;code&gt;spacexstockanalysis.com&lt;/code&gt;. Between them they declare 4,868 URLs in their sitemaps.&lt;/p&gt;

&lt;p&gt;Partial collapses are the harder ones. &lt;code&gt;howtobuyspacexstock.com&lt;/code&gt; scored four out of six: &lt;code&gt;/investor-resources/&lt;/code&gt;, &lt;code&gt;/how-to-buy/&lt;/code&gt; and &lt;code&gt;/stock-price/&lt;/code&gt; all returned the 8,090-byte homepage, while its &lt;a href="https://howtobuyspacexstock.com/guides/where-to-buy-spacex-stock/" rel="noopener noreferrer"&gt;guide to the routes that actually exist for buying SpaceX stock&lt;/a&gt; still ships 22,217 bytes of its own markup. Half the sitemap works, which is precisely why nobody would go looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  4,868 declared URLs, one real page
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;spacexstockanalysis.com&lt;/code&gt; is the cleanest example. Its sitemap lists 17 URLs. Every one I requested came back as the same 8,749 bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;200  8749B  https://spacexstockanalysis.com/spacex-stock-symbol/
200  8749B  https://spacexstockanalysis.com/spacex-stock-price/
200  8749B  https://spacexstockanalysis.com/spacex-stock-history/
200  8749B  https://spacexstockanalysis.com/spacex-stock-secondary-market/
200  8749B  https://spacexstockanalysis.com/spacex-stock-starlink/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seventeen URLs. One document. The sitemap says "here are seventeen things." The server says "here is the same thing, seventeen times."&lt;/p&gt;

&lt;p&gt;That page is also where the canonical tag gets interesting. Its &lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt; points at &lt;code&gt;https://spacexstockanalysis.com/&lt;/code&gt; — the homepage. Which is honest, in a way. But the same document also carries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"robots"&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"index, follow, max-snippet:-1,
      max-image-preview:large, max-video-preview:-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Index me. Follow me. But canonicalize me to a different URL. The page is arguing with itself in two adjacent &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; lines.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nextgemcrypto.com&lt;/code&gt; is worse, because it doesn't even offer a canonical escape hatch. Four thousand seven hundred twenty-seven URLs in the sitemap, zero &lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt; anywhere in the 53,861-byte document that answers all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search Console calls this "indexed"
&lt;/h2&gt;

&lt;p&gt;This is the part that changed how I read coverage reports.&lt;/p&gt;

&lt;p&gt;Across the 19 soft-404 hosts, the page-indexing export GSC gives you (15 URLs sampled per property) contained 133 rows marked &lt;code&gt;Submitted and indexed&lt;/code&gt;. I fetched 68 of them — five per host where available — and hashed each response against its host's homepage.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checked&lt;/th&gt;
&lt;th&gt;Byte-identical to homepage&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;68 URLs&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;57.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fifty-seven percent of the URLs Google called indexed were serving me the homepage, byte for byte, with an &lt;code&gt;index, follow&lt;/code&gt; directive and no canonical pointing anywhere else.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;openaistocknews.com&lt;/code&gt; is the extreme: 15 of 15 sampled URLs covered, and all five I fetched returned the same 10,528 bytes. GSC says fifteen pages indexed. The site has one page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s https://openaistocknews.com/openai-sec-filing | wc -c
10528
$ curl -s https://openaistocknews.com/nvidia-ai-stock  | wc -c
10528
$ curl -s https://openaistocknews.com/chatgpt-ipo-price | wc -c
10528
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Indexed" in Search Console means Google stored &lt;em&gt;something&lt;/em&gt; for that URL. It does not mean the URL resolves to a distinct document, and it definitely does not mean the URL can appear in results. Once the canonical cluster collapses fourteen URLs into one homepage, you have coverage numbers and no rankings, and nothing in the UI connects those two facts.&lt;/p&gt;

&lt;p&gt;The distribution it &lt;em&gt;does&lt;/em&gt; show, if you go looking:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Coverage state&lt;/th&gt;
&lt;th&gt;URLs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Submitted and indexed&lt;/td&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL is unknown to Google&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discovered – currently not indexed&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crawled – currently not indexed&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternate page with proper canonical tag&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate, Google chose different canonical&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page with redirect&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those last three rows are the collapse, itemised. Five pages Google decided were alternates of something else. Two where Google overrode the site's own canonical choice. And 133 where it stored a homepage under a URL that was supposed to be an article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four signals, four answers
&lt;/h2&gt;

&lt;p&gt;For a single URL like &lt;code&gt;spacexstockanalysis.com/spacex-stock-symbol/&lt;/code&gt;, the stack says four different things at once:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;200 OK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;this resource exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sitemap&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; entry&lt;/td&gt;
&lt;td&gt;this is a distinct page, please crawl it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;meta name="robots"&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;index, follow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;please index this page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://…/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;this is actually a different page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A crawler has to pick one. Google's answer, broadly, is: fetch it, see the body is identical to another URL, follow the canonical, and mark the loser as an alternate. You keep your 200s, lose the URLs, and never see an error anywhere.&lt;/p&gt;

&lt;p&gt;That is a hard failure mode to notice, because every surface reports success. The status code is success. The sitemap submitted cleanly. The coverage report is mostly green. Only the SERP disagrees, and the SERP doesn't have a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;The mechanism is almost always a rewrite rule that was written for a single-page app and never revisited when the site became something else.&lt;/p&gt;

&lt;p&gt;Netlify and Cloudflare Pages both accept this in &lt;code&gt;_redirects&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/*    /index.html    200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifteen characters of intent, and every URL on the host forever answers with the app shell. It is the correct rule for a client-routed SPA with no server-side rendering, where the shell genuinely is the response and JavaScript resolves the view. It is wrong the moment your pages are server-rendered, statically generated, or — as in &lt;code&gt;spacexstockanalysis.com&lt;/code&gt; — supposed to be seventeen distinct documents.&lt;/p&gt;

&lt;p&gt;A Next.js catch-all does the same thing without a config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/[...slug]/page.js  -- catches everything, renders the homepage&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;CatchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HomePage&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody writes that on purpose. It gets written as a placeholder, ships, and then quietly outlives its purpose by a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is smaller than the bug
&lt;/h2&gt;

&lt;p&gt;Three lines, depending on your host.&lt;/p&gt;

&lt;p&gt;For a static host with a real 404 document, delete the catch-all line and ship the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/*    /index.html    200    &amp;lt;-- delete this, ship a 404.html at the root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For nginx, make the fallback terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;try_files $uri $uri/&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Next.js, call &lt;code&gt;notFound()&lt;/code&gt; instead of rendering something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;notFound&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"next/navigation"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// renders not-found.js with a real 404&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Article&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify it the way a crawler would, not the way your test suite would:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"https://{host}/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"https://{host}{p}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns anything but &lt;code&gt;(0, n)&lt;/code&gt;, you have a soft 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;p&gt;I fixed the routing on the five six-out-of-six hosts first, because a sitemap that declares 4,868 URLs and resolves to one document is not a sitemap, it is a liability. Then I regenerated the sitemaps from the actual route table rather than from a slug generator, which is how &lt;code&gt;nextgemcrypto.com&lt;/code&gt; ended up with 4,727 &lt;code&gt;/keyword/&lt;/code&gt; URLs that never had a page behind them.&lt;/p&gt;

&lt;p&gt;The remaining hosts still have the fallback. I'm leaving them for a week to see whether the 404s change the coverage numbers — that's the part I don't have data for yet, and I'd rather report the before/after than guess.&lt;/p&gt;

&lt;p&gt;Two things I'd check on any site this week, both cheap:&lt;/p&gt;

&lt;p&gt;First, request a URL you know is missing and look at the status code, not the page. A 200 with a nice error message is still a 200.&lt;/p&gt;

&lt;p&gt;Second, take five URLs from your sitemap and hash them against your homepage. If any two match, you have URLs that Google will crawl, index, and never rank — and no report anywhere will tell you.&lt;/p&gt;

&lt;p&gt;The full probe took 68 seconds for 114 hosts. It found 21 sites answering every missing URL with the front page, 61 of 64 measured 200s that were byte-identical to a homepage, and a coverage export that marked 133 such URLs as indexed. The status code was never going to tell me any of that. I had to read the bytes.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>http</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>Our robots.txt bans 31 crawlers. I tested 7: two got through, and Baidu got a 520.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Tue, 10 Nov 2026 02:17:46 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/our-robotstxt-bans-31-crawlers-i-tested-7-two-got-through-and-baidu-got-a-520-27mo</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/our-robotstxt-bans-31-crawlers-i-tested-7-two-got-through-and-baidu-got-a-520-27mo</guid>
      <description>&lt;p&gt;114 sites, one robots.txt template, 31 banned user agents. I probed 7 of them and got three different answers.&lt;/p&gt;

&lt;p&gt;The network I audit runs 114 sites behind the same edge. Every one of them serves a robots.txt. I had never read one end to end, because I assumed the file was boring. It is 2,756 characters at the median and half of it is a legal notice.&lt;/p&gt;

&lt;p&gt;So I pulled all 114, parsed them, then did the thing robots.txt never gets: I tested whether the bans actually fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  The file is half legal notice
&lt;/h2&gt;

&lt;p&gt;All 114 returned 200. All 114 are text/plain. None of them accidentally served an HTML 404 page, which is where this kind of audit usually starts.&lt;/p&gt;

&lt;p&gt;Here is the shape of a median file, by character count:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Characters&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Comment block (EU DSM Article 4 reservation)&lt;/td&gt;
&lt;td&gt;1,392&lt;/td&gt;
&lt;td&gt;50.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;# BEGIN Cloudflare Managed content&lt;/code&gt; marker&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;1.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User-agent groups and rules&lt;/td&gt;
&lt;td&gt;1,230&lt;/td&gt;
&lt;td&gt;44.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trailing blank lines&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;3.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The comment block is not junk. It is a rights reservation under Article 4 of the EU Copyright in the Digital Single Market directive, explaining what &lt;code&gt;search&lt;/code&gt;, &lt;code&gt;ai-input&lt;/code&gt;, &lt;code&gt;ai-train&lt;/code&gt; and &lt;code&gt;use&lt;/code&gt; mean before the rules start. It is written for a lawyer, not for a crawler.&lt;/p&gt;

&lt;p&gt;But the split matters for a different reason. When a file is 51% prose, nobody reads the other 49%. I know this because I wrote the deployment and I did not read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thirty-one identical bans across 114 files
&lt;/h2&gt;

&lt;p&gt;The union of banned user agents across all 114 files is 31. Each individual file contains exactly 31. Not 30, not 32 — the same 31 strings, in the same order, byte for byte, in all 114 files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Amazonbot, Applebot-Extended, Bytespider, CCBot, ClaudeBot, Diffbot,
Google-Extended, GPTBot, omgili, anthropic-ai, Claude-Web, cohere-ai,
MistralAI-Training, meta-externalagent, GoogleOther, Baiduspider,
PetalBot, AwarioRssBot, Google-CloudVertexBot, QualifiedBot, Cotoyogi,
ICC-Crawler, atlassian-bot, FishBot, BorderxBot, NavuBot,
SemrushBot-SWA, WARDBot, magpie-crawler, KimiBot, CitibotSiteCrawler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of those 31 gets the same two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Baiduspider&lt;/span&gt;
&lt;span class="nc"&gt;Disallow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a full-site ban. No path escaped, no rate limit, no partial allowance.&lt;/p&gt;

&lt;p&gt;Read the list again. Roughly sixteen of those names are AI training or grounding crawlers, which is the stated intent. The rest are not. &lt;code&gt;Baiduspider&lt;/code&gt; is Baidu's search crawler. &lt;code&gt;PetalBot&lt;/code&gt; is Huawei's search crawler. &lt;code&gt;GoogleOther&lt;/code&gt; is Google's miscellaneous fetcher, not Gemini training. &lt;code&gt;atlassian-bot&lt;/code&gt; fetches link previews. &lt;code&gt;SemrushBot-SWA&lt;/code&gt; is a site audit tool. &lt;code&gt;ICC-Crawler&lt;/code&gt; is an academic index.&lt;/p&gt;

&lt;p&gt;One list, two very different jobs, and the search crawlers are on the wrong side of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wildcard group forbids nothing
&lt;/h2&gt;

&lt;p&gt;Here is the part that surprised me more than the ban list. Every file has two &lt;code&gt;User-agent: *&lt;/code&gt; groups — 113 of the 114 files, anyway. The first sits at the top of the managed block, right under the Content-Signal line. The second sits at the very bottom, after &lt;code&gt;# END Cloudflare Managed content&lt;/code&gt;, where the site's own config gets appended.&lt;/p&gt;

&lt;p&gt;Both of them contain exactly one directive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="nc"&gt;Allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is not a single &lt;code&gt;Disallow&lt;/code&gt; in either wildcard group. Across all 114 files. So the effective policy for any crawler that is not one of the 31 named agents is: crawl everything, no restrictions, no crawl-delay.&lt;/p&gt;

&lt;p&gt;The file is 2,756 characters long and imposes zero constraints on general crawling. It is a blocklist wearing the costume of a policy document.&lt;/p&gt;

&lt;p&gt;Every file also carries the same content signal — &lt;code&gt;search=yes,ai-train=no,use=reference&lt;/code&gt; — in all 114. So the machine-readable intent is consistent. The rules underneath it are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two status codes, two different layers
&lt;/h2&gt;

&lt;p&gt;Reading rules tells you what the operator intended. It does not tell you what the edge does. So I sent 14 user agents at 6 domains in the same minute, same URL, same headers apart from the UA string. The result was identical on all 6 domains.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User agent&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Response body&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome 131 (control)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;77,104 bytes of HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random crawler, not on any list (control)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;full page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Googlebot&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;full page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bingbot&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;full page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Applebot&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;full page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GoogleOther&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;full page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PetalBot&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;full page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPTBot&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Your request was blocked.&lt;/code&gt; (25 bytes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClaudeBot&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Your request was blocked.&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CCBot&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Your request was blocked.&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bytespider&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Your request was blocked.&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baiduspider&lt;/td&gt;
&lt;td&gt;520&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;error code: 520&lt;/code&gt; (16 bytes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AhrefsBot&lt;/td&gt;
&lt;td&gt;520&lt;/td&gt;
&lt;td&gt;&lt;code&gt;error code: 520&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SemrushBot&lt;/td&gt;
&lt;td&gt;520&lt;/td&gt;
&lt;td&gt;&lt;code&gt;error code: 520&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two control probes matter here. The random crawler UA — a name that appears in no list anywhere — gets a 200 with the full page. So this is not a blanket "block all bots" rule that catches unknown agents by heuristics. It is name matching against a list.&lt;/p&gt;

&lt;p&gt;And the two responses come from two different places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;403&lt;/strong&gt; is the edge deciding. The body is a 25-byte sentence written by the WAF rule. The request never reaches the origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;520&lt;/strong&gt; is the edge reporting that the origin did not answer usefully. &lt;code&gt;error code: 520&lt;/code&gt; is Cloudflare's own body for "web server returned an unknown error". The request passed the edge and the origin returned nothing usable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction is the whole article. Same declared policy, two enforcement mechanisms, and they behave differently in ways that matter to a crawler.&lt;/p&gt;

&lt;p&gt;Here is the probe, if you want to run it against your own sites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;

&lt;span class="no"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"chrome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "&lt;/span&gt;
                 &lt;span class="s2"&gt;"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"googlebot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"baidu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s2"&gt;"Mozilla/5.0 (compatible; Baiduspider/2.0; "&lt;/span&gt;
                 &lt;span class="s2"&gt;"+http://www.baidu.com/search/spider.html)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"gptbot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; "&lt;/span&gt;
                 &lt;span class="s2"&gt;"GPTBot/1.2; +https://openai.com/gptbot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="s2"&gt;"Mozilla/5.0 (compatible; ZzqRandomCrawler/1.0; +https://example.invalid/bot)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"User-Agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Accept"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"text/html,*/*"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ua&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{name:10} {status} {size:&amp;gt;7}B  {head!r}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;unknown&lt;/code&gt; row is the one people skip. Without it you cannot tell a name-based blocklist from a generic bot heuristic, and those two have completely different consequences when you add a new crawler to your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bans that never fired
&lt;/h2&gt;

&lt;p&gt;Of the 31 declared bans, I tested 7 by name. Five fired. Two did not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PetalBot&lt;/code&gt; and &lt;code&gt;GoogleOther&lt;/code&gt; are both named with &lt;code&gt;Disallow: /&lt;/code&gt; in all 114 files. Both got a 200 and a full page. The declaration exists; nothing enforces it. If you are relying on robots.txt alone to keep either of them out, you are not keeping them out.&lt;/p&gt;

&lt;p&gt;Then the reverse. &lt;code&gt;AhrefsBot&lt;/code&gt; and &lt;code&gt;SemrushBot&lt;/code&gt; are not in the list. The list has &lt;code&gt;SemrushBot-SWA&lt;/code&gt;, which is the site-audit variant, but not the main &lt;code&gt;SemrushBot&lt;/code&gt;. Neither name appears anywhere in the 2,756 characters. Both got a 520.&lt;/p&gt;

&lt;p&gt;So the blocklist and the enforcement layer disagree in both directions. Things that were banned were let in. Things that were never mentioned were shut out. Whatever is generating the file and whatever is enforcing it at runtime are not reading from the same source, and nobody has reconciled them since the template was applied.&lt;/p&gt;

&lt;p&gt;This is the same failure mode as a forecast that declares 70% confidence and delivers 62% — the declaration is not the behaviour, and the gap only shows up when you score outcomes instead of reading claims. I have been running that measurement on a set of forecasting experiments, and the method is identical: state the claim, then test it, then compare. A &lt;a href="https://predictionlabcn.com/experiments/lab-04-calibration-experiment/" rel="noopener noreferrer"&gt;calibration experiment that scores stated confidence against realised outcomes&lt;/a&gt; makes the point faster than any audit report: the gap is usually in the places nobody instrumented.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 520 is the wrong way to say no
&lt;/h2&gt;

&lt;p&gt;If the goal is "do not crawl me", 403 is the correct answer. It is a 4xx: a permanent, explicit refusal. Crawlers treat it as an instruction and stop.&lt;/p&gt;

&lt;p&gt;520 is a 5xx. To every crawler on the internet, a 5xx means "the server is broken, try again later". Google's documentation is explicit that persistent 5xx responses cause crawl rate reduction and, eventually, removal from the index. Baidu behaves the same way, with less patience.&lt;/p&gt;

&lt;p&gt;So the sites that meant to block an AI crawler are instead telling Baidu that they are down.&lt;/p&gt;

&lt;p&gt;I ran the Baiduspider probe three times against five domains, four seconds apart, to rule out a transient origin failure. Fifteen requests, fifteen 520s. Then I tried &lt;code&gt;Baiduspider-render&lt;/code&gt;, the rendering variant: 520 as well.&lt;/p&gt;

&lt;p&gt;Then I tried the sitemap. &lt;code&gt;https://&amp;lt;domain&amp;gt;/sitemap-index.xml&lt;/code&gt; with the Baiduspider UA returned 520 too. Not the sitemap contents — the same 16-byte error line.&lt;/p&gt;

&lt;p&gt;Whatever is producing that 520 is upstream of the routing, so it does not distinguish a homepage from a sitemap from a stylesheet. From Baidu's perspective the entire host is unreachable, and there is no diagnostic it can act on, because 520 carries no information about what went wrong.&lt;/p&gt;

&lt;p&gt;The sites that got a clean 403 are in better shape than the ones that got a 520, and both of them are worse off than the ones that got a 200 when they should not have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sitemap was in perfect shape the whole time
&lt;/h2&gt;

&lt;p&gt;Here is the part that stings. Of the 114 sites, 113 declare a &lt;code&gt;Sitemap:&lt;/code&gt; line in robots.txt. I fetched all 113 and checked the root element, not just the status code — a 200 that returns HTML is a much more common failure than a 404.&lt;/p&gt;

&lt;p&gt;113 out of 113 returned real XML. 83 were &lt;code&gt;&amp;lt;urlset&amp;gt;&lt;/code&gt;, 30 were &lt;code&gt;&amp;lt;sitemapindex&amp;gt;&lt;/code&gt;, all served as &lt;code&gt;text/xml&lt;/code&gt;. Correct content type, correct root element, parseable, reachable with a normal user agent.&lt;/p&gt;

&lt;p&gt;So the discovery chain was intact. The sitemap was well-formed, declared in robots.txt, and served correctly. The only thing standing between that sitemap and a search engine was a 520 that fires before routing.&lt;/p&gt;

&lt;p&gt;That is the worst shape a technical problem can take: every component passes its own unit test, and the pipeline still delivers nothing. When I want to know what a system actually does rather than what its components claim, I go to the primary record — the way &lt;a href="https://spacexstockanalysis.com/spacex-form-d-filings/" rel="noopener noreferrer"&gt;the Form D filings behind a funding number&lt;/a&gt; tell you more than any press summary of it. The status code is the primary record here. The robots.txt is the press release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking your own network
&lt;/h2&gt;

&lt;p&gt;Three checks, in order of how often they find something:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Does the ban fire?&lt;/strong&gt; Take every agent in your robots.txt and probe your homepage with that exact UA string. Compare against a Chrome control and an unknown-crawler control. Three outcomes, three different bugs: 200 means the ban is decorative, 403 means it works, 5xx means it works in the worst possible way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Is anything blocked that you never declared?&lt;/strong&gt; The unknown-crawler control catches blanket rules. Then probe your analytics tools, your SEO tools, your uptime monitor. &lt;code&gt;AhrefsBot&lt;/code&gt; and &lt;code&gt;SemrushBot&lt;/code&gt; were being refused on 114 sites that never named them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Is your wildcard group actually restrictive?&lt;/strong&gt; Print it. If it is one &lt;code&gt;Allow: /&lt;/code&gt; with no &lt;code&gt;Disallow&lt;/code&gt;, your file is a blocklist and nothing else — and every new crawler that appears next quarter will walk straight in until someone adds it by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robotparser&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;RobotFileParser&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;txt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/robots.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"User-Agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RobotFileParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Googlebot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Bingbot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Baiduspider"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"PetalBot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"GPTBot"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{agent:12} allowed={rp.can_fetch(agent, 'https://example.com/')}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RobotFileParser&lt;/code&gt; alone gets you the declaration side in about ten lines. It will tell you &lt;code&gt;PetalBot&lt;/code&gt; is banned. It will not tell you &lt;code&gt;PetalBot&lt;/code&gt; walked in anyway, which is why the probe matters more than the parse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am changing
&lt;/h2&gt;

&lt;p&gt;Two fixes, in this order.&lt;/p&gt;

&lt;p&gt;First the 520. Any refusal that returns 5xx for a named crawler should be a 403 or nothing at all. A 5xx is a lie about your own uptime, and search engines bill you for it in crawl budget before they bill you in rankings.&lt;/p&gt;

&lt;p&gt;Second the list. Search crawlers and training crawlers should not share an allow/deny list, because they have opposite economics: one brings you traffic, the other takes your content. &lt;code&gt;Baiduspider&lt;/code&gt;, &lt;code&gt;PetalBot&lt;/code&gt; and &lt;code&gt;GoogleOther&lt;/code&gt; come off the list. They do not train models. Keeping them in the same block as &lt;code&gt;GPTBot&lt;/code&gt; costs index coverage and buys nothing.&lt;/p&gt;

&lt;p&gt;The template itself is the harder problem. A managed block that is identical across 114 files is a good default and a bad policy — it encodes one vendor's opinion about which agents matter, and it ships that opinion to every property without asking whether the property agrees. The content layer already works this way: &lt;a href="https://coinwikipro.com/coin/akash-network" rel="noopener noreferrer"&gt;a coin page on one of the sites&lt;/a&gt; carries the same header, the same analytics script and the same caveats as the other 492 entries in that sitemap, because the template decided for all of them. Every site in the network inherited 31 decisions nobody made.&lt;/p&gt;

&lt;p&gt;I have 114 sites and 3,534 declared bans across them. Five of the seven I tested were real. The other two were furniture, and the one that mattered most was answered with a 5xx.&lt;/p&gt;

&lt;p&gt;Half the file was a legal notice. I should probably have read the other half sooner.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>http</category>
      <category>security</category>
    </item>
    <item>
      <title>104 of 114 homepages ship no images at all. Their only image is a 270 KB social card.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Mon, 09 Nov 2026 02:17:19 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/104-of-114-homepages-ship-no-images-at-all-their-only-image-is-a-270-kb-social-card-2nlp</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/104-of-114-homepages-ship-no-images-at-all-their-only-image-is-a-270-kb-social-card-2nlp</guid>
      <description>&lt;p&gt;I crawled the home pages of 114 sites that all belong to the same small publishing network — prediction-market and crypto-ticker properties, mostly Next.js and mostly Cloudflare. Every one answered HTTP 200. Between them, the HTML contains &lt;strong&gt;727 &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;104 of those 114 pages contain none of them.&lt;/p&gt;

&lt;p&gt;That number surprised me enough to re-run the crawl three different ways before I believed it: parsing &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; with a regex, checking for lazy-loading attributes and &lt;code&gt;srcset&lt;/code&gt;, and then separately fetching every linked stylesheet to see whether the images were hiding in CSS instead. They mostly weren't. The median page is 60,452 characters of HTML with no image reference anywhere in it.&lt;/p&gt;

&lt;p&gt;Here is the counting code, if you want to point it at your own site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;

&lt;span class="n"&gt;IMG_RE&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;img\b([^&amp;gt;]*)&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ATTR_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*=\s*(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;([^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]*)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;([^&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]*)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;|([^\s&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;]+))&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;attrs_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ATTR_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;imgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;IMG_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;attrs_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-src&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urljoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loading&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loading&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;srcset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;srcset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  104 pages, zero images
&lt;/h2&gt;

&lt;p&gt;Of the 114 pages, 104 returned an empty list. The remaining ten split in a lopsided way: six pages carry ten &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags between them, and four pages carry &lt;strong&gt;717&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I then asked a harder question: forget &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, does the page reference &lt;em&gt;any&lt;/em&gt; image at all? So I added three more sources — &lt;code&gt;url()&lt;/code&gt; inside inline &lt;code&gt;style&lt;/code&gt; attributes, &lt;code&gt;url()&lt;/code&gt; inside &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks, and &lt;code&gt;url()&lt;/code&gt; inside the linked stylesheets (192 sheets fetched, 188 OK) — plus a count of inline &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; elements.&lt;/p&gt;

&lt;p&gt;The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;37 pages&lt;/strong&gt; reference no image in any of those five places. No &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, no CSS background, no inline SVG. Just text, a stylesheet, and a script tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;43 pages&lt;/strong&gt; have no &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; and no CSS image, but do carry inline SVG — a median of 26 icons each, drawn as paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;24 pages&lt;/strong&gt; do pull images from CSS, 26 files in total across the whole network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;52 pages&lt;/strong&gt; use inline SVG somewhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the "no images" story is really an "images were replaced by icon paths" story. Forty-three pages are drawing their visual furniture as &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; rather than shipping files. That is a defensible choice for icons: one request fewer, no extra format negotiation, crisp at any size.&lt;/p&gt;

&lt;p&gt;It stops working the moment you want a photograph.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four pages that do ship &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The four are two templates cloned once each — a &lt;code&gt;coinatlas*&lt;/code&gt; pair and a &lt;code&gt;coinwiki*&lt;/code&gt; pair. They are also the only pages where you can measure what image delivery looks like when someone actually attempts it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;what I checked&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags on the four pages&lt;/td&gt;
&lt;td&gt;717 (98.6% of all tags in the network)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;images served from a third-party CDN&lt;/td&gt;
&lt;td&gt;717 (all &lt;code&gt;coin-images.coingecko.com&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loading="lazy"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;722 of 727 (99.3%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no &lt;code&gt;loading&lt;/code&gt; attribute at all&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loading="eager"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;fetchpriority&lt;/code&gt; on any image&lt;/td&gt;
&lt;td&gt;0 of 727&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; both present&lt;/td&gt;
&lt;td&gt;7 of 727 (1.0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;srcset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 of 727 (0.4%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.png&lt;/code&gt; / &lt;code&gt;.jpg&lt;/code&gt; / &lt;code&gt;.webp&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;80.5% / 18.0% / 0.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;image displayed in a 28 px circle (&lt;code&gt;h-7 w-7&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;202&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Seven hundred of those images are coin logos pulled from CoinGecko's &lt;code&gt;large&lt;/code&gt; bucket, which I fetched and measured at 250×250 and 12,242 bytes for BTC, rendered inside a 28 px round element. The 202 carrying the literal class &lt;code&gt;h-7 w-7 rounded-full&lt;/code&gt; are the visible subset on the home page; the rest sit in coin tables further down.&lt;/p&gt;

&lt;p&gt;Every single one is lazy. Not one declares its intrinsic size. That combination is the classic layout-shift recipe: a 28 px box that reserves no space until a 250 px PNG arrives and the browser relayouts around it. It also means every one of those 250×250 files is decoded and scaled down to a 28 px slot — about 80× the pixels actually painted, on 700 images.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;srcset&lt;/code&gt; story is worse than the percentage suggests. Three images have it. All 717 point at an external host, so the network controls neither the variant set nor the cache headers, and has no fallback if that path changes. If CoinGecko reshapes those URLs again, 717 images break at once — in the same deploy, on four sites, with no local copy to fall back to. Pages like &lt;a href="https://coinwikipro.com/coin/aave" rel="noopener noreferrer"&gt;the Aave listing on one of those templates&lt;/a&gt; would be the first place you'd see it: 13 images, every one of them rented.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only image a crawler ever fetches
&lt;/h2&gt;

&lt;p&gt;If 104 pages have no &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, what image does anyone ever see? The answer is &lt;code&gt;og:image&lt;/code&gt; — the link-preview card. That is the whole image surface of this network.&lt;/p&gt;

&lt;p&gt;40 pages declare one. 74 don't. And this is where the numbers get genuinely bad, because I downloaded all 40 and read the pixel dimensions out of the file headers (PNG &lt;code&gt;IHDR&lt;/code&gt; at byte 16, JPEG &lt;code&gt;SOFn&lt;/code&gt; after the marker scan):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x89&lt;/span&gt;&lt;span class="s"&gt;PNG&lt;/span&gt;&lt;span class="se"&gt;\r\n\x1a\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;II&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\xff\xd8\xff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xC0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xC1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xC2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xC3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;HH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xD8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xD9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mh"&gt;0xD0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mh"&gt;0xD7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;H&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of the 40 declared cards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;36 resolve&lt;/strong&gt;, 4 don't: two return 404, two return 403 to a plain GET (the files may exist behind bot protection — either way a preview fetcher sees nothing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15.65 MB total.&lt;/strong&gt; Median 270 KB. Largest &lt;strong&gt;1.34 MB&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;18 of 36 are over 300 KB&lt;/strong&gt;, 12 are over 500 KB.&lt;/li&gt;
&lt;li&gt;28 PNG, 7 JPEG, 1 SVG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;19 are ~1.91:1&lt;/strong&gt; (the shape every platform wants), &lt;strong&gt;11 are 1:1 squares&lt;/strong&gt;, 4 are ~3:2, 2 are other.&lt;/li&gt;
&lt;li&gt;One is &lt;strong&gt;48×48&lt;/strong&gt; — a favicon being used as a social card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 48×48 PNG declared as your link preview is the same category of mistake as &lt;code&gt;loading="lazy"&lt;/code&gt; on your hero image: it works in the sense that no error is thrown, and it is wrong in every way that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  10 of 20 cards are lying about their own size
&lt;/h2&gt;

&lt;p&gt;22 pages also declare &lt;code&gt;og:image:width&lt;/code&gt; and &lt;code&gt;og:image:height&lt;/code&gt;. Of the 20 whose values parse as integers, &lt;strong&gt;10 disagree with the actual file&lt;/strong&gt;. Every one of the 10 declares 1200×630:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;page&lt;/th&gt;
&lt;th&gt;declared&lt;/th&gt;
&lt;th&gt;actual&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;marketoraclehub.com&lt;/td&gt;
&lt;td&gt;1200×630&lt;/td&gt;
&lt;td&gt;1731×909&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;marketpredictionlab.com&lt;/td&gt;
&lt;td&gt;1200×630&lt;/td&gt;
&lt;td&gt;1024×1024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openaistockforecast.com&lt;/td&gt;
&lt;td&gt;1200×630&lt;/td&gt;
&lt;td&gt;1024×1024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spacexstocktarget.com&lt;/td&gt;
&lt;td&gt;1200×630&lt;/td&gt;
&lt;td&gt;1024×1024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;predictmarketcn.com&lt;/td&gt;
&lt;td&gt;1200×630&lt;/td&gt;
&lt;td&gt;1200×640&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is obvious once you see it: someone copied the canonical 1200×630 snippet into a shared &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; component, then generated a square 1024×1024 image with a different tool. The metadata was never regenerated. Ten pages, five of them asserting a dimension the file does not have.&lt;/p&gt;

&lt;p&gt;This isn't cosmetic. Platforms that read &lt;code&gt;og:image:width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt; use them to reserve the card's box before the image arrives and to decide cropping; when the values are wrong, you get a card that either letterboxes or crops the interesting part off. And the ones that ignore the declared values and re-read the file get the square version, which in a feed becomes a center crop of a 1024×1024 composition — usually through the title text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketpredictionlab.com/predictions/ai/openai-valuation/" rel="noopener noreferrer"&gt;One of the square-card pages&lt;/a&gt; is a good example: the card is a designed square with the logo centered, and every platform that consumes it will crop it to 1.91:1 and slice the top and bottom off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weight is the part I'd fix first
&lt;/h2&gt;

&lt;p&gt;Median 270 KB for an image whose only job is to sit in a 600×314 preview box. A 1200×630 card at quality 80 is usually somewhere between 60 and 90 KB as JPEG and well under that as WebP, so this network is paying three to four times the going rate.&lt;/p&gt;

&lt;p&gt;Aggregate: 15.65 MB across 36 files, for a network whose entire HTML payload — 114 pages, median 60,452 characters — adds up to about 6.9 million characters. The social cards weigh more than twice the site.&lt;/p&gt;

&lt;p&gt;And there is a neat irony in where the heaviest one lives. The biggest card, 1.34 MB, belongs to &lt;a href="https://spacexstocktarget.com/articles/spacex-valuation-analysis-2026/" rel="noopener noreferrer"&gt;a page about SpaceX's valuation&lt;/a&gt; — a 178,012-character HTML document that contains zero &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags, paired with a 1.34 MB PNG that only a crawler will ever download.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I think this matters
&lt;/h2&gt;

&lt;p&gt;Three reasons, in order of how much I'd care:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Image search is a separate index.&lt;/strong&gt; Pages with no &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; and no CSS background contribute nothing to it. For a network publishing "what is a prediction market" explainers, that's a whole query surface left on the table — and it's the one surface where a small site can still rank on day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Every share renders as a random crop or a blank box.&lt;/strong&gt; 4 of 40 cards are unreachable, 11 are square, 10 declare the wrong size, one is a 48×48 favicon. When someone pastes a link into Slack or X, the card is the first impression, and for a large share of this network the first impression is a cropped square or nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The pages that do use images use them in the worst available way.&lt;/strong&gt; 717 &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags, 99.3% lazy, 1% with intrinsic dimensions, 0.8% WebP, all of them from a third-party host whose paths the network doesn't control. That is not a small inefficiency; it's a layout-stability and availability dependency disguised as a design decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it on your own site
&lt;/h2&gt;

&lt;p&gt;This is the whole check, and it takes about 40 lines. It tells you whether your social card exists, how heavy it is, and whether your declared dimensions match the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;property=[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']og:image[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\'][^&amp;gt;]*content=[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']([^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']+)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content=[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']([^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']+)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\'][^&amp;gt;]*property=[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']og:image[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;og_image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;declared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;og:image:width[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\'][^&amp;gt;]*content=[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\'](\d+)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dims&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ratio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;declared_width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;declared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;declared&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;declared&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;declared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it on your own home page before you run it on anyone else's. The &lt;code&gt;ok&lt;/code&gt; flag is the one that earns its keep: it's the only check here that compares what you claim with what you actually shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do, in this order
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Generate the card at 1200×630, export WebP with a JPEG fallback, target under 100 KB. One template, one output.&lt;/li&gt;
&lt;li&gt;Delete &lt;code&gt;og:image:width&lt;/code&gt; and &lt;code&gt;og:image:height&lt;/code&gt; from the shared &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; unless they are generated from the same source as the image. Wrong metadata is worse than absent metadata.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; to every &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;. For the 28 px coin logos that's &lt;code&gt;width="28" height="28"&lt;/code&gt; and a &lt;code&gt;srcset&lt;/code&gt; with a 56 px variant — the current 250 px source is 8× the pixels needed.&lt;/li&gt;
&lt;li&gt;Stop lazy-loading anything above the fold, and put &lt;code&gt;fetchpriority="high"&lt;/code&gt; on the one image that is the LCP candidate. Zero of the 727 images here declare a priority, so the browser discovers them all at the same, late, moment.&lt;/li&gt;
&lt;li&gt;For the 37 pages with no image at all: pick one page, add one real photograph with &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, &lt;code&gt;srcset&lt;/code&gt; and meaningful &lt;code&gt;alt&lt;/code&gt;, and watch the image-search impressions for a month. That's the experiment the rest of this network is missing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The thing I keep coming back to is that none of this is exotic. These are five attributes and one image pipeline. The network has 114 pages, 60 KB of HTML each, and a well-built static generator behind it — the infrastructure is fine. What's missing is that nobody ever opened the network tab on a share card.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>seo</category>
      <category>html</category>
    </item>
    <item>
      <title>The 7.8 KB tracker got an ETag, a 12-hour TTL and an edge HIT. The page got none of them.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Sun, 08 Nov 2026 02:04:19 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/the-78-kb-tracker-got-an-etag-a-12-hour-ttl-and-an-edge-hit-the-page-got-none-of-them-3nah</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/the-78-kb-tracker-got-an-etag-a-12-hour-ttl-and-an-edge-hit-the-page-got-none-of-them-3nah</guid>
      <description>&lt;p&gt;The tracking script that 109 of 114 pages load is 7,850 bytes, sits behind the same CDN, and answers with an &lt;code&gt;ETag&lt;/code&gt;, a &lt;code&gt;Cache-Control: max-age=43200&lt;/code&gt;, and &lt;code&gt;cf-cache-status: HIT&lt;/code&gt;. The pages that load it answer with &lt;code&gt;Last-Modified&lt;/code&gt; and nothing else — no &lt;code&gt;ETag&lt;/code&gt;, no &lt;code&gt;Cache-Control&lt;/code&gt;, and &lt;code&gt;cf-cache-status: DYNAMIC&lt;/code&gt; on every single one.&lt;/p&gt;

&lt;p&gt;I audited the home page of all 114 sites in one content network. 113 of them send no &lt;code&gt;Cache-Control&lt;/code&gt; header at all. One sends &lt;code&gt;public, max-age=300&lt;/code&gt;. Zero send an &lt;code&gt;ETag&lt;/code&gt;. All 114 send &lt;code&gt;Last-Modified&lt;/code&gt;, all 114 keep that value stable across two requests, and all 114 answer a conditional request with &lt;code&gt;304&lt;/code&gt; and a zero-byte body.&lt;/p&gt;

&lt;p&gt;So the plumbing works. What's missing is the decision — how long this response may be reused without asking anyone. Nobody on any of these 114 hosts has made that decision, and the browser is quietly making it instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;For each of the 114 hosts, over plain &lt;code&gt;urllib&lt;/code&gt;, no headless browser and no Lighthouse:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One &lt;code&gt;GET /&lt;/code&gt; with &lt;code&gt;Accept-Encoding: gzip, deflate, br&lt;/code&gt;, recording status, time to first byte, wire bytes, and the cache-relevant response headers.&lt;/li&gt;
&lt;li&gt;A second &lt;code&gt;GET /&lt;/code&gt; to compare &lt;code&gt;Last-Modified&lt;/code&gt; — a validator that changes between two requests five seconds apart is worse than no validator.&lt;/li&gt;
&lt;li&gt;A third &lt;code&gt;GET /&lt;/code&gt; carrying &lt;code&gt;If-None-Match&lt;/code&gt; and &lt;code&gt;If-Modified-Since&lt;/code&gt; built from the first response.&lt;/li&gt;
&lt;li&gt;Up to five &lt;code&gt;&amp;lt;link rel="stylesheet"&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; URLs from each page, fetched for their own cache policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The pages are static documents of a few hundred words, the kind of thing that should be the easiest caching problem on the internet: &lt;a href="https://cryptobullforecast.com/learn/what-is-a-prediction-market/" rel="noopener noreferrer"&gt;an explainer on how prediction markets price an event&lt;/a&gt;, a fee schedule that changes once a year, a market page for a contract that does not settle until 2027. Content that is expensive to regenerate and cheap to store.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit is short
&lt;/h2&gt;

&lt;p&gt;The whole conditional-request test fits in a function. The only subtle part is not sending &lt;code&gt;Accept-Encoding&lt;/code&gt; when you want to parse the HTML you just downloaded — I got &lt;code&gt;br&lt;/code&gt; back, tried to regex it as text, and got zero assets out of 114 pages on the first pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (compatible; cache-audit/1.0)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Encoding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gzip, deflate, br&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# drop this if you need the body as text
&lt;/span&gt;    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extra&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttfb_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# headers received, body not
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cond&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;etag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If-None-Match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;etag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last-modified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If-Modified-Since&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last-modified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cond&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache-control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;etag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;etag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_modified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last-modified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cf_cache_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cf-cache-status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first_ttfb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttfb_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cond_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cond_ttfb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttfb_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cond_bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that against a page and the answer comes back in one line: &lt;code&gt;{"cache_control": None, "etag": None, "last_modified": "Sat, 31 Oct 2026 11:09:50 GMT", "cf_cache_status": "DYNAMIC", "first_ttfb": 1498, "cond_status": 304, ...}&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 114 pages sent
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;Pages sending it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Last-Modified&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;114 / 114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cache-Control&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 / 114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ETag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 / 114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cf-cache-status: DYNAMIC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;114 / 114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Content-Encoding: br&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;114 / 114&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three separate layers, three different verdicts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compression is fine.&lt;/strong&gt; Every page came back brotli-encoded with &lt;code&gt;Vary: Accept-Encoding&lt;/code&gt;, and the median raw HTML of 73,641 bytes arrived as 10,578 bytes on the wire. That's an 86% cut, applied uniformly, with no exceptions to hunt down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation is fine, and better than fine.&lt;/strong&gt; 114 out of 114 answered &lt;code&gt;304&lt;/code&gt; with a body of exactly zero bytes. Not one page re-sent a 10 KB document to confirm it hadn't changed. Whatever else is wrong here, the machine that decides "unchanged" is working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freshness is absent.&lt;/strong&gt; One page out of 114 said how long its response stays valid. The other 113 handed the browser a validator and no policy, which per RFC 9111 §4.2.2 means the browser is allowed to invent one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 304 is cheap in bytes and expensive in time
&lt;/h2&gt;

&lt;p&gt;Here is the part that surprised me. The &lt;code&gt;304&lt;/code&gt; responses came back in a median of 1,391 ms. The full responses came back in a median of 1,467 ms — a difference of 86 ms, about 6%.&lt;/p&gt;

&lt;p&gt;The 304 saved 10,578 bytes of compressed transfer and almost no time. That makes sense once you see where the 304 came from: &lt;code&gt;cf-cache-status: DYNAMIC&lt;/code&gt; on all 114 pages means the CDN did not have the page, did not store it, and forwarded the request to the origin. The origin is what decides the page is unchanged, and the origin is what takes 1.4 seconds.&lt;/p&gt;

&lt;p&gt;A conditional request that has to reach the origin inherits the origin's latency. You saved bandwidth on a document that was already compressed to 10 KB, and you paid the full round trip to do it.&lt;/p&gt;

&lt;p&gt;Absolute numbers here are from a single vantage point in Asia against a network whose origin is elsewhere, so treat the milliseconds as directional. The header values are not: &lt;code&gt;DYNAMIC&lt;/code&gt; on 114 of 114 is a configuration fact, not a network measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same network gets the assets right
&lt;/h2&gt;

&lt;p&gt;The tracker at &lt;code&gt;coinpriceoracle.com/t.js&lt;/code&gt; is loaded by 109 of these 114 pages, cross-domain, on a different host than the one serving the page. It gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;cache-control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;max-age=43200&lt;/span&gt;
&lt;span class="na"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;W/"6a79412e-1eaa"&lt;/span&gt;
&lt;span class="na"&gt;last-modified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mon, 10 Aug 2026 03:10:38 GMT&lt;/span&gt;
&lt;span class="na"&gt;cf-cache-status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HIT&lt;/span&gt;
&lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2118&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;ETag&lt;/code&gt;, a 12-hour TTL, an edge HIT with a 35-minute age, 7,850 bytes. Every one of those headers is correct for a file that gets deployed rather than generated.&lt;/p&gt;

&lt;p&gt;Across 407 stylesheet and script samples from the same 114 sites: 292 served from the edge cache, 63 MISS, 39 REVALIDATED, one DYNAMIC. The asset layer is configured. The document layer was never touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 12-hour TTL on filenames that never change
&lt;/h2&gt;

&lt;p&gt;Of those 407 asset samples, 358 — 88% — carry &lt;code&gt;max-age=43200&lt;/code&gt;. That's twelve hours, and it is the correct default &lt;em&gt;only&lt;/em&gt; if the URL changes when the bytes change.&lt;/p&gt;

&lt;p&gt;These URLs do not change. They look like &lt;code&gt;/site.css&lt;/code&gt;, &lt;code&gt;/all.min.css&lt;/code&gt;, &lt;code&gt;/remixicon.min.css&lt;/code&gt;, &lt;code&gt;/event-reporting.min.js&lt;/code&gt;, &lt;code&gt;/t.js&lt;/code&gt;. Exactly two of the 407 filenames contained a content hash, both from a Next.js build that ships &lt;code&gt;/_next/static/chunks/…&lt;/code&gt; whether you ask it to or not.&lt;/p&gt;

&lt;p&gt;Twelve hours on an un-versioned filename is a specific kind of bug, and it's the kind you only notice from the other side. Ship a CSS fix at 09:00 and a visitor who loaded the site at 08:50 keeps the old stylesheet until 20:50 — same URL, still fresh in their cache, no request sent, no way for them to know. The fix is live and invisible. With &lt;code&gt;immutable&lt;/code&gt; on top of a hashed name, none of that happens: new bytes mean a new URL, so a one-year TTL is free.&lt;/p&gt;

&lt;p&gt;The inverse is also true: if the URL is stable, the TTL has to be short, and then you need &lt;code&gt;stale-while-revalidate&lt;/code&gt; so the short TTL doesn't turn into a blocking round trip on every deploy day. A page like &lt;a href="https://polymarkettutorials.com/polymarket-fees.html" rel="noopener noreferrer"&gt;this comparison of Polymarket's fee tiers&lt;/a&gt; is regenerated whenever the template changes and edited maybe twice a year. It is the textbook case for a five-minute TTL with a one-day stale window: nobody notices the five minutes, and nobody waits for the origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Heuristic caching" is the browser guessing on your behalf
&lt;/h2&gt;

&lt;p&gt;With no &lt;code&gt;Cache-Control&lt;/code&gt;, RFC 9111 lets a cache assign freshness heuristically, and the classic implementation is 10% of the time since &lt;code&gt;Last-Modified&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The median &lt;code&gt;Last-Modified&lt;/code&gt; age across these 114 pages was 6.7 days, so the heuristic lands around 16 hours. Comfortable. But the value is not a policy, it's arithmetic on a timestamp the deployment wrote — and it moves with the content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The most recently deployed page in the set had a &lt;code&gt;Last-Modified&lt;/code&gt; of 0.99 days old. Its heuristic freshness is about 2.4 hours.&lt;/li&gt;
&lt;li&gt;37 of 114 pages carried a &lt;code&gt;Last-Modified&lt;/code&gt; older than 30 days, which the heuristic happily turns into three days of staleness.&lt;/li&gt;
&lt;li&gt;Right after a deploy, &lt;code&gt;Last-Modified&lt;/code&gt; is seconds old and the heuristic window collapses to seconds. The site is &lt;em&gt;least&lt;/em&gt; cacheable exactly when a release has just put it under load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the heuristic can't tell the difference between those cases. A page whose content is pinned to a settlement date — &lt;a href="https://forecastnewshub.com/markets/btc-150k-q1-2027/" rel="noopener noreferrer"&gt;the BTC 150k Q1 2027 contract page&lt;/a&gt; — gets the same arithmetic as a page rebuilt hourly, because the only input is a timestamp that says nothing about whether the bytes will change.&lt;/p&gt;

&lt;p&gt;None of these numbers were chosen by anyone. They're side effects of a timestamp, interpreted by a spec clause that exists so caches have something to do when the origin declines to say.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would send instead
&lt;/h2&gt;

&lt;p&gt;Three policies, chosen by one question: can this URL's content change without the URL changing?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;

&lt;span class="n"&gt;HASHED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/_next/static/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/assets/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/static/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cache_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;has_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# bytes cannot change without the URL changing
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public, max-age=31536000, immutable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;css&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;js&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;woff2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;svg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avif&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# stable filename: short TTL, serve stale while revalidating
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public, max-age=300, stale-while-revalidate=86400&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# HTML: let the edge hold a short copy, keep browsers revalidating
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public, max-age=0, s-maxage=60, stale-while-revalidate=86400&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public, max-age=0, s-maxage=60, stale-while-revalidate=86400&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hashed_asset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public, max-age=31536000, immutable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stable_asset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public, max-age=300, stale-while-revalidate=86400&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two numbers doing the work are &lt;code&gt;s-maxage&lt;/code&gt; and &lt;code&gt;stale-while-revalidate&lt;/code&gt;. &lt;code&gt;s-maxage=60&lt;/code&gt; is what turns &lt;code&gt;cf-cache-status: DYNAMIC&lt;/code&gt; into a HIT: the edge may hold the page for a minute and serve it to the next hundred visitors without touching the origin. &lt;code&gt;stale-while-revalidate=86400&lt;/code&gt; means that a day past expiry, the edge still answers instantly from the stale copy and refreshes in the background — so the 1,391 ms revalidation happens off the critical path instead of inside a navigation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ETag&lt;/code&gt; matters more than it looks here. &lt;code&gt;Last-Modified&lt;/code&gt; has one-second resolution, so two deploys inside the same second produce the same validator and a client can be told "unchanged" about bytes that changed. A content hash has no such failure mode, and it's cheaper than it sounds: the body is already in memory when you're setting headers.&lt;/p&gt;

&lt;p&gt;On Cloudflare specifically, none of this requires touching the application. A Cache Rule that matches HTML and sets &lt;code&gt;s-maxage&lt;/code&gt; plus &lt;code&gt;stale-while-revalidate&lt;/code&gt; gets the edge half; the &lt;code&gt;ETag&lt;/code&gt; still has to come from the origin, because the edge will not invent one for a response it forwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not prove
&lt;/h2&gt;

&lt;p&gt;I forced the conditional request. Whether a real browser sends one at all depends on the heuristic freshness it computed for itself — which, as above, is a number nobody chose. So the honest claim is not "every returning visitor costs you 1.4 seconds." It's "whenever a browser or an edge decides to check, the check costs a full origin round trip, and nothing in the response tells it how long to wait before checking again."&lt;/p&gt;

&lt;p&gt;I also didn't measure whether any of this affects rankings, and I'm not going to imply it does. Core Web Vitals measure what a visitor experiences, and a visitor who has never been to the site before pays the origin latency regardless of cache headers. What the headers change is the second visit, the hundredth visitor, and what happens to your origin on the morning you deploy.&lt;/p&gt;

&lt;p&gt;The build that would have caught all of it is nine lines in CI: request each page, assert &lt;code&gt;cache-control&lt;/code&gt; is present, assert &lt;code&gt;etag&lt;/code&gt; or &lt;code&gt;last-modified&lt;/code&gt; exists, assert the conditional request returns &lt;code&gt;304&lt;/code&gt;, and assert every hashed asset has a TTL over a year. Four assertions, run against staging, before the config goes out. The 114 pages above would have failed the first one 113 times.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>http</category>
      <category>caching</category>
    </item>
    <item>
      <title>A search box needed titles. The page sent 300 full records, 41,000 characters of meta descriptions.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Sat, 07 Nov 2026 02:10:58 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/a-search-box-needed-titles-the-page-sent-300-full-records-41000-characters-of-meta-descriptions-28l6</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/a-search-box-needed-titles-the-page-sent-300-full-records-41000-characters-of-meta-descriptions-28l6</guid>
      <description>&lt;p&gt;A single search box on one home page was backed by 255,881 characters of serialized records. The box matches on titles. 106,706 of those characters were fields it can never display.&lt;/p&gt;

&lt;p&gt;I crawled the home page of 114 sites in one content network, captured the raw HTML before any JavaScript ran, and measured how much of that HTML was Next.js App Router's inline Flight payload — the &lt;code&gt;self.__next_f.push(...)&lt;/code&gt; calls that carry the React Server Component tree to the client.&lt;/p&gt;

&lt;p&gt;30 of the 114 sites ran App Router. Across those 30 home pages, 48.2% of the combined HTML was Flight payload, and 3.5% was visible text. For comparison, the 84 sites that shipped plain server-rendered HTML landed at 16.5% visible text.&lt;/p&gt;

&lt;p&gt;That gap is the whole story, and it has two different causes. Both are measurable, and both are fixable without abandoning the framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the measurement works
&lt;/h2&gt;

&lt;p&gt;No headless browser, no Lighthouse. Just &lt;code&gt;urllib&lt;/code&gt; and a regex against the raw response, so every number below can be reproduced by anyone with &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Flight payload in an App Router HTML response arrives as a series of calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__next_f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$Sreact.fragment&lt;/span&gt;&lt;span class="se"&gt;\"\n&lt;/span&gt;&lt;span class="s2"&gt;2:I[35176,[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;/_next/static/chunks/…&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;],&lt;/span&gt;&lt;span class="se"&gt;\"\"&lt;/span&gt;&lt;span class="s2"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each argument is a JSON string. Concatenating them in document order reconstructs the RSC tree as text. Then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;payload characters&lt;/strong&gt; = length of that concatenation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;visible text&lt;/strong&gt; = &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; minus &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;, tags collapsed, whitespace normalized&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;structural HTML&lt;/strong&gt; = the document with every &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block removed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third number matters more than the first two. It tells you what the page actually painted, independent of what it also shipped for the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30 sites, by the numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median HTML&lt;/td&gt;
&lt;td&gt;133,637 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median visible text&lt;/td&gt;
&lt;td&gt;4,801 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median text share&lt;/td&gt;
&lt;td&gt;3.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total HTML (30 pages)&lt;/td&gt;
&lt;td&gt;4,697,963 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total Flight payload&lt;/td&gt;
&lt;td&gt;2,265,107 characters (48.2%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload share, median site&lt;/td&gt;
&lt;td&gt;49.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sites above 50% payload&lt;/td&gt;
&lt;td&gt;13 of 30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload after gzip&lt;/td&gt;
&lt;td&gt;334,172 characters (85.2% compression)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;className&lt;/code&gt; occurrences&lt;/td&gt;
&lt;td&gt;11,694, of which 3,168 unique&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serialized &lt;code&gt;children&lt;/code&gt; nodes&lt;/td&gt;
&lt;td&gt;12,566&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Against the 84 server-rendered sites: median HTML 50,586 bytes, median visible text 4,510 characters, median text share 16.5%.&lt;/p&gt;

&lt;p&gt;The visible text is roughly the same. The markup around it is 2.6× larger. That is not a content difference — it is a serialization difference.&lt;/p&gt;

&lt;p&gt;You can see the other end of the range in &lt;a href="https://spacexstockprediction.com/broker-comparison/" rel="noopener noreferrer"&gt;a broker comparison page that serves 2.4 KB compressed and still fills a screen&lt;/a&gt;. Nothing about that page is unmaintainable; it just never had a component tree written into it twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 1: the payload is data, and most of the data is unused
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;marketpredictionnow.com&lt;/code&gt; is the extreme case. Its home page is 666,510 bytes, of which 281,038 characters are Flight payload — 65.2% of the document. Strip the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; blocks and 118,007 characters of markup remain. Visible text: 5,939 characters, or 1.4% of the page.&lt;/p&gt;

&lt;p&gt;Inside that payload sit 300 complete content records, 255,881 characters total. Each record carries 19 fields: &lt;code&gt;slug&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;excerpt&lt;/code&gt;, &lt;code&gt;seoTitle&lt;/code&gt;, &lt;code&gt;seoDescription&lt;/code&gt;, &lt;code&gt;background&lt;/code&gt;, &lt;code&gt;bullCase&lt;/code&gt;, &lt;code&gt;bearCase&lt;/code&gt;, &lt;code&gt;watchMetrics&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, &lt;code&gt;deadline&lt;/code&gt;, &lt;code&gt;published&lt;/code&gt;, &lt;code&gt;updated&lt;/code&gt;, &lt;code&gt;category&lt;/code&gt;, &lt;code&gt;entity&lt;/code&gt;, &lt;code&gt;evidenceLevel&lt;/code&gt;, &lt;code&gt;searchIntent&lt;/code&gt;, &lt;code&gt;editorScore&lt;/code&gt;, &lt;code&gt;scoreTrend&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The page renders 27 of them as cards. I checked all 300 titles against the script-free HTML: 27 matched, 273 did not. Those 273 records occupy 233,157 characters that never reach the document.&lt;/p&gt;

&lt;p&gt;The obvious defence is the search box — and there is one, a single &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; with a placeholder reading "搜索比特币、AI、英伟达……". So the records are not gratuitous. But a title search does not need this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Rendered&lt;/th&gt;
&lt;th&gt;Characters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seoDescription&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 / 300&lt;/td&gt;
&lt;td&gt;28,884&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bullCase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 / 300&lt;/td&gt;
&lt;td&gt;14,705&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;watchMetrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 / 300&lt;/td&gt;
&lt;td&gt;13,523&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bearCase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 / 300&lt;/td&gt;
&lt;td&gt;13,158&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 / 300&lt;/td&gt;
&lt;td&gt;12,811&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seoTitle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 / 300&lt;/td&gt;
&lt;td&gt;12,301&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;published&lt;/code&gt; + &lt;code&gt;updated&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0 / 300&lt;/td&gt;
&lt;td&gt;7,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;searchIntent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 / 300&lt;/td&gt;
&lt;td&gt;3,080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;evidenceLevel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 / 300&lt;/td&gt;
&lt;td&gt;1,044&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is 106,706 characters — 42% of all record data — for fields that are neither rendered nor searchable in any useful sense. &lt;code&gt;seoDescription&lt;/code&gt; alone is 28,884 characters of meta description text sent to a browser that will never display it; its only consumer is the crawler, and the crawler already had it in the &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;The fields a client-side title filter can actually use are &lt;code&gt;title&lt;/code&gt; (8,321), &lt;code&gt;slug&lt;/code&gt; (8,052) and &lt;code&gt;category&lt;/code&gt; (2,055): 18,428 characters, or 7.2% of what was shipped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;REC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^{}]*?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;searchIntent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;REC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="n"&gt;recs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;usable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;usable&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;recs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;total&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;recs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;needed&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;needed&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 18,428 of 255,881 = 7.2%
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those records are also a real content library, and a single article page is where that library should be read — &lt;a href="https://spacexstocktoday.com/article/spacex-competitors-analysis/" rel="noopener noreferrer"&gt;one SpaceX competitor analysis rendered on its own route&lt;/a&gt; does the same job without carrying the index with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 2: the payload is markup, and the markup is repetitive
&lt;/h2&gt;

&lt;p&gt;The second pattern has no data in it at all. &lt;code&gt;cryptogemradar.com&lt;/code&gt; ships 131,295 characters of payload on a 254,845-byte home page, and it contains 1,184 &lt;code&gt;className&lt;/code&gt; values — 110 of them unique. A single string, &lt;code&gt;text-primary&lt;/code&gt;, appears 177 times. &lt;code&gt;flex items-center gap-2 whitespace-nowrap text-sm&lt;/code&gt; appears 168 times.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;openaistockforecast.com&lt;/code&gt; is the same shape: 184,049 characters of payload, 906 &lt;code&gt;className&lt;/code&gt; values across 138 unique ones, with &lt;code&gt;text-sm text-muted-foreground transition-colors hover:text-foreground&lt;/code&gt; repeated 83 times. Its payload also carries long RSC path strings like &lt;code&gt;$e:props:children:0:props:children:1:props:children:props:children:0:props:iconNode&lt;/code&gt;, which exist purely to address nodes in the tree being serialized.&lt;/p&gt;

&lt;p&gt;Across all 30 sites, 11,694 &lt;code&gt;className&lt;/code&gt; occurrences collapse to 3,168 unique strings. Deduplicating them would remove 495,736 characters — 21.9% of the total payload — without changing a single pixel.&lt;/p&gt;

&lt;p&gt;This is what the serialization is actually spending its budget on: not content, not state, but the styling of decorative containers, written out again for each instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  gzip hides this, and that is the trap
&lt;/h2&gt;

&lt;p&gt;The payload compresses at 85.2%. 2.27 million characters become 334 KB across 30 pages — about 11 KB per page over the wire. At that size, nobody's Core Web Vitals dashboard moves, which is exactly why this survives for years.&lt;/p&gt;

&lt;p&gt;What gzip does not compress away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parse cost.&lt;/strong&gt; 131,295 characters of Flight text still has to be parsed and walked to build the tree. &lt;code&gt;marketpredictionnow.com&lt;/code&gt; asks the browser to materialize 12,566 &lt;code&gt;children&lt;/code&gt; nodes for a page that draws 27 cards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unused 42%.&lt;/strong&gt; Meta descriptions and bull/bear cases that never render are not made cheaper by being compressed; they are just smaller waste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cacheability.&lt;/strong&gt; Every one of these responses came back &lt;code&gt;cf-cache-status: DYNAMIC&lt;/code&gt; with no &lt;code&gt;Cache-Control&lt;/code&gt; header, so the HTML — payload included — is regenerated and re-sent on every request instead of being served from the edge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the one I would fix first. A page whose content changes daily does not need to be re-serialized on every visit.&lt;/p&gt;

&lt;h2&gt;
  
  
  A CI check you can run in ten seconds
&lt;/h2&gt;

&lt;p&gt;You do not need to crawl 114 sites to catch this. One request per route in your own build is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;PUSH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;self\.__next_f\.push\(\[1,(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:[^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\\]|\\.)*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)\]\)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Encoding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gzip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload-budget/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1f\x8b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PUSH&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;body\b[^&amp;gt;]*&amp;gt;(.*)&amp;lt;/body&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;[^&amp;gt;]+&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;(script|style|svg)\b[^&amp;gt;]*&amp;gt;.*?&amp;lt;/\1&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;share&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;share&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;share&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;share&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it into the build and fail when &lt;code&gt;share&lt;/code&gt; crosses 0.4. On the 30 sites above, 13 would have failed. Pick the budget from your own baseline rather than from mine — a dashboard with real client state has a legitimate reason to carry a large tree; a static article page does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four fixes, in order of payoff
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trim the query, not the framework.&lt;/strong&gt; If a route serializes N records, select only the columns the rendered view and the client filter need. On &lt;code&gt;marketpredictionnow.com&lt;/code&gt; that alone removes 106,706 characters, and the search box still works because it only ever matched on titles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paginate the index.&lt;/strong&gt; Inlining 300 records to render 27 is a 10:1 ratio. Inline the 27, fetch the rest on interaction. The full set belongs on the index route, not the home page — &lt;a href="https://coinforecastpro.com/en/defi-trends-2026/" rel="noopener noreferrer"&gt;a DeFi trends article delivered as one document&lt;/a&gt; carries no listing data at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let the edge cache.&lt;/strong&gt; Set an explicit &lt;code&gt;Cache-Control&lt;/code&gt; with a short &lt;code&gt;s-maxage&lt;/code&gt; and a long &lt;code&gt;stale-while-revalidate&lt;/code&gt;. A 666 KB document that is regenerated per request is the expensive part, not the 15 KB of compressed payload inside it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop serializing decorative class strings.&lt;/strong&gt; If a component's only prop is a Tailwind string, it does not need to round-trip through Flight. This is the 495,736 characters that deduplicate to nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this measurement does not prove
&lt;/h2&gt;

&lt;p&gt;The payload is not dead weight by design. On an App Router page it is what lets a client-side navigation reuse an already-loaded tree instead of issuing a fresh round trip. On a site where visitors click from page to page, that is a genuine win, and I am not arguing Next.js is the wrong choice for these 30 sites — the 84 server-rendered ones have their own failure modes, several of which I have written about separately.&lt;/p&gt;

&lt;p&gt;What the numbers do show is narrower and harder to argue with: on pages that are read once and left, roughly half the document is a second copy of the page, and on one page 106,706 characters of that copy described a dataset whose only consumer was a single search input.&lt;/p&gt;

&lt;p&gt;The 84 plain sites are not a control group I chose for purity — they run the same content pipeline. They are just 2.6× lighter for the same amount of text, and that gap is entirely a serialization decision.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>performance</category>
      <category>react</category>
    </item>
    <item>
      <title>26 of 114 sitemaps gave every URL the same lastmod. One repeated a millisecond 312 times.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Fri, 06 Nov 2026 02:09:36 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/26-of-114-sitemaps-gave-every-url-the-same-lastmod-one-repeated-a-millisecond-312-times-3e5a</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/26-of-114-sitemaps-gave-every-url-the-same-lastmod-one-repeated-a-millisecond-312-times-3e5a</guid>
      <description>&lt;p&gt;I read 30,353 &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; entries out of 114 sitemaps. On 26 of those domains, every single URL carried the same &lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt; value. One domain repeated an identical timestamp — down to the millisecond — 312 times.&lt;/p&gt;

&lt;p&gt;That is not a freshness signal. That is a build log line that wandered into an XML file.&lt;/p&gt;

&lt;p&gt;Here is the count, the two ways it happens, and the ledger that fixes it without inventing dates you cannot defend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually collected
&lt;/h2&gt;

&lt;p&gt;For each domain I fetched the root &lt;code&gt;sitemap.xml&lt;/code&gt;, followed child sitemaps one level deep, and parsed every &lt;code&gt;&amp;lt;url&amp;gt;&lt;/code&gt; block for its &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt;. No page crawling yet — this is purely what the site tells a crawler before it fetches anything.&lt;/p&gt;

&lt;p&gt;114 domains, 30,353 URLs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;36 domains&lt;/strong&gt; ship no &lt;code&gt;lastmod&lt;/code&gt; at all — 9,718 URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;65 domains&lt;/strong&gt; date every URL. 13 date only part of the sitemap, leaving 5,177 URLs undated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;26 domains&lt;/strong&gt; put a single value across every URL they date — 3,904 URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;13 of those 26&lt;/strong&gt; share the literal string &lt;code&gt;2026-10-14&lt;/code&gt; — 2,714 URLs on 13 different hostnames.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Sitemap has lastmod" sounds like a checked box. What the parser sees is one value repeated N times, and a parser can tell the difference between a date and a constant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test is a counter
&lt;/h2&gt;

&lt;p&gt;You do not need a crawler to catch this. You need &lt;code&gt;Counter&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="n"&gt;URL_BLOCK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;url&amp;gt;(.*?)&amp;lt;/url&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;LOC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;loc&amp;gt;\s*(.*?)\s*&amp;lt;/loc&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;LASTMOD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;lastmod&amp;gt;\s*(.*?)\s*&amp;lt;/lastmod&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lastmod_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;URL_BLOCK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LOC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;lm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LASTMOD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lm&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;present&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;present&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;top_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;with_lastmod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;present&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distinct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top_value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;top_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top_share&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_n&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;present&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;present&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it on a sitemap that is edited by humans and &lt;code&gt;distinct&lt;/code&gt; tracks the number of editing sessions. Run it on a collapsed one and &lt;code&gt;distinct&lt;/code&gt; is &lt;code&gt;1&lt;/code&gt; whether the site has 40 URLs or 1,114. The discriminator is &lt;code&gt;top_share == 1.0&lt;/code&gt; with more than a handful of URLs — cheap enough to assert in CI on every deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode one: frozen at the first render
&lt;/h2&gt;

&lt;p&gt;One domain, 312 URLs, every one stamped &lt;code&gt;2026-07-18T12:19:02.782Z&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Milliseconds are the tell. A human editing a page does not produce &lt;code&gt;.782&lt;/code&gt;. A file write does — and this one still matches the filesystem: the same page's HTTP &lt;code&gt;Last-Modified&lt;/code&gt; is &lt;code&gt;Sat, 18 Jul 2026 12:19:02 GMT&lt;/code&gt;. The sitemap copied the mtime of the build output, then never updated it.&lt;/p&gt;

&lt;p&gt;Sampling twelve pages from that domain found three distinct &lt;code&gt;Last-Modified&lt;/code&gt; values spanning 105 days, including a homepage rebuilt on 31 October. The sitemap still tells Google that homepage last changed on 18 July. The content had moved on; the timestamp had not. That is the worst combination, because a stale date on a page that &lt;em&gt;did&lt;/em&gt; change is a page Google has no reason to revisit — &lt;a href="https://coinatlashub.com/guide/how-to-buy-bitcoin-usa-2026/" rel="noopener noreferrer"&gt;a 2026 guide to buying Bitcoin in the US&lt;/a&gt; on that domain is a clean example of a page whose bytes have not moved since the July build.&lt;/p&gt;

&lt;p&gt;The usual cause is generating &lt;code&gt;lastmod&lt;/code&gt; from &lt;code&gt;os.path.getmtime()&lt;/code&gt; of the rendered file. That works exactly once: the next build rewrites every file, and if your pipeline is smarter than a &lt;code&gt;touch&lt;/code&gt;, the mtime stops advancing at all — which is exactly how you end up frozen in July.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode two: stamped with the build date, not the content date
&lt;/h2&gt;

&lt;p&gt;The opposite failure looks healthier and is just as useless. One domain has 1,114 URLs, but only 3 of them carry a &lt;code&gt;lastmod&lt;/code&gt; at all — all three stamped &lt;code&gt;2026-10-31&lt;/code&gt;, while two sampled pages from the same domain return &lt;code&gt;Last-Modified: Thu, 15 Oct 2026 13:34:24 GMT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Partial coverage is its own bug, and it hides the collapse: the counter sees one distinct value on three URLs and reports a perfectly informative ratio. It is not informative. It is three URLs out of 1,114, forward-dated by 16 days, sitting on a sitemap that dates almost nothing.&lt;/p&gt;

&lt;p&gt;The full-coverage version of the same failure is easier to see. Thirteen domains in this set produced byte-identical &lt;code&gt;2026-10-14&lt;/code&gt; across 2,714 URLs — 941 of them on a single host. That is what a shared template deploy looks like from the outside: one build, many hostnames, one timestamp smeared over all of them. A domain outside that group went further and put &lt;code&gt;2026-10-07T00:00:00.000Z&lt;/code&gt; — midnight, zero microseconds — on all 66 of the URLs it dates, including &lt;a href="https://marketforecastdaily.com/faq/" rel="noopener noreferrer"&gt;its FAQ page&lt;/a&gt;, which is the sort of page that changes roughly never. The other 524 URLs in that sitemap have no date at all.&lt;/p&gt;

&lt;p&gt;Both failure modes trip the same counter. They are not the same bug, and the fix is not the same: one needs the timestamp to advance, the other needs it to stop advancing in lockstep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server already disagrees with the sitemap
&lt;/h2&gt;

&lt;p&gt;There is a free second opinion on every response: the &lt;code&gt;Last-Modified&lt;/code&gt; header. I paired it with &lt;code&gt;lastmod&lt;/code&gt; only where the sitemap actually dated that exact URL — 115 pages across 60 domains. They disagreed by more than a day on 85 of them: 62 headers newer than the sitemap claim, 23 older, 30 within a day.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Dated URLs&lt;/th&gt;
&lt;th&gt;Sitemap says&lt;/th&gt;
&lt;th&gt;Page &lt;code&gt;Last-Modified&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;openaistocknews.com&lt;/td&gt;
&lt;td&gt;41 / 41&lt;/td&gt;
&lt;td&gt;2025-07-01&lt;/td&gt;
&lt;td&gt;2026-11-02&lt;/td&gt;
&lt;td&gt;489 days stale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cryptobullforecast.com&lt;/td&gt;
&lt;td&gt;116 / 116&lt;/td&gt;
&lt;td&gt;2026-06-01&lt;/td&gt;
&lt;td&gt;2026-10-31&lt;/td&gt;
&lt;td&gt;152 days stale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;coinatlashub.com&lt;/td&gt;
&lt;td&gt;312 / 312&lt;/td&gt;
&lt;td&gt;2026-07-18&lt;/td&gt;
&lt;td&gt;2026-10-31&lt;/td&gt;
&lt;td&gt;105 days stale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nextcryptopump.com&lt;/td&gt;
&lt;td&gt;270 / 270&lt;/td&gt;
&lt;td&gt;2026-10-14&lt;/td&gt;
&lt;td&gt;2026-06-02&lt;/td&gt;
&lt;td&gt;134 days ahead&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row is a domain still advertising July 2025 as its last change while serving files written in November 2026. That is not a rounding error in a hint field; it is a site telling a crawler, in a machine-readable format, that sixteen months of work did not happen.&lt;/p&gt;

&lt;p&gt;The last row is the inverse, and it is the one people miss: the sitemap claims a modification 134 days &lt;em&gt;after&lt;/em&gt; the file it describes. Nothing was edited on 14 October — a batch job ran and stamped its own clock across 270 entries.&lt;/p&gt;

&lt;p&gt;The inverse case is just as easy to find on a bigger sitemap. On a domain whose 327 dated entries all claim 14 October, three sampled market pages carried &lt;code&gt;Last-Modified: Mon, 02 Nov 2026 18:35:15 GMT&lt;/code&gt; — nineteen days of edits the sitemap does not admit to. Pages like &lt;a href="https://marketpredictionnews.com/forecasts/crypto/highest-probability/" rel="noopener noreferrer"&gt;that domain's list of highest-probability crypto forecasts&lt;/a&gt; are regenerated on a schedule, and the only field that could have told a crawler so is pinned to a single value.&lt;/p&gt;

&lt;h2&gt;
  
  
  What lastmod is and is not
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;lastmod&lt;/code&gt; is not a ranking factor. It is a scheduling hint: it lets a crawler spend a recrawl budget on pages that moved instead of pages that did not. That is why a constant is worse than nothing. A constant carries zero bits, so the crawler falls back to its own heuristics — and the published guidance is that &lt;code&gt;lastmod&lt;/code&gt; is only used when it is consistently and verifiably accurate. Inconsistency does not degrade one entry; it teaches the crawler to discount the field for the whole host.&lt;/p&gt;

&lt;p&gt;Note the asymmetry in the numbers above: 36 domains shipped no &lt;code&gt;lastmod&lt;/code&gt; at all. That is a legitimate, documented choice. Nobody is penalised for omitting it. The only unrecoverable option is the fabricated one, because once a crawler has evidence that your dates are decorative, restoring trust takes far longer than deleting the field would have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: hash what you render, publish when the hash changed
&lt;/h2&gt;

&lt;p&gt;Do not compute &lt;code&gt;lastmod&lt;/code&gt; at render time. Compute it once, when the content actually changes, and store the answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;STRIP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;(script|style|noscript)[^&amp;gt;]*&amp;gt;.*?&amp;lt;/\1&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;MAIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;(article|main)[^&amp;gt;]*&amp;gt;(.*?)&amp;lt;/\1&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;[^&amp;gt;]+&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ENTITIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;[a-z#0-9]+;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;content_fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Hash the part of the page a reader would notice changing.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MAIN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;STRIP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[a-z0-9&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ENTITIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;DB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lastmod.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;CREATE TABLE IF NOT EXISTS page (
    url TEXT PRIMARY KEY,
    fingerprint TEXT NOT NULL,
    changed_at TEXT NOT NULL
)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;content_fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT fingerprint, changed_at FROM page WHERE url = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,)).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# first sighting: the honest value is the publish date, not today
&lt;/span&gt;        &lt;span class="n"&gt;changed_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timespec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO page VALUES (?, ?, ?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;changed_at&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;changed_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timespec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE page SET fingerprint = ?, changed_at = ? WHERE url = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;changed_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;changed_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="c1"&gt;# unchanged: keep the old date
&lt;/span&gt;    &lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;changed_at&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details matter more than the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fingerprint the content, not the file.&lt;/strong&gt; A template change to the footer, the cookie banner, or a nav link rewrites every byte of every page. Hashing the &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt; container after stripping scripts and styles means cosmetic deploys do not touch a single &lt;code&gt;lastmod&lt;/code&gt;. If your pages have no semantic container, hash the inline text minus a fixed list of chrome selectors — but pick something, because hashing the whole document guarantees permanent churn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clamp to reality.&lt;/strong&gt; &lt;code&gt;lastmod&lt;/code&gt; must never exceed the time you actually published. A build that backfills archives should write the archive's real date, not the deploy's. If you do not know the date, omit &lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt; for that URL — the protocol allows per-URL omission, and crawlers handle missing values far better than wrong ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One row per canonical URL.&lt;/strong&gt; If two paths canonicalise to one, they share one fingerprint row. Otherwise the same content gets two clocks and you have rebuilt the bug in a different shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  A CI check that fails when the field collapses
&lt;/h2&gt;

&lt;p&gt;The counter from earlier is the assertion. Run it against the generated sitemap before deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_informative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_distinct_ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distinct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;with_lastmod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distinct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;                      &lt;span class="c1"&gt;# omitting lastmod is allowed
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;distinct&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; URLs, 1 distinct lastmod: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;top_value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;distinct&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_distinct_ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;distinct&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; distinct values over &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; URLs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top_share&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;top_share&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of URLs share one timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A healthy content site fails none of these. A site where every page really was published in one import will fail them honestly, which is the correct outcome: those pages have one real date, and the sitemap should say so once rather than pretend to be a change feed.&lt;/p&gt;

&lt;p&gt;Add a coverage assertion too, because the 1,114-URL domain above would pass the collapse check on three dated URLs: &lt;code&gt;with_lastmod / urls&lt;/code&gt; should be either 1.0 or 0.0, never 0.27. And pair a sample against the response header — &lt;code&gt;abs(Last-Modified - lastmod) &amp;lt; 1 day&lt;/code&gt; — which is the only one of the three checks that catches a build stamp applied to everything while the build runs often.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not fix
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;lastmod&lt;/code&gt; will not get a page indexed. If the page is &lt;code&gt;noindex&lt;/code&gt;, canonicalised elsewhere, or orphaned, an accurate timestamp changes nothing — and in this same fleet, none of 180 sampled pages carried a &lt;code&gt;noindex&lt;/code&gt; at all, which means the timestamp was the only freshness signal on offer and 26 domains spent it on a constant.&lt;/p&gt;

&lt;p&gt;The countermeasure is one function, one table, and one test. What it buys is narrow and real: when a page changes, the crawl budget goes there. 30,353 URLs is a lot of budget to spend on a number that never moves.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>python</category>
      <category>http</category>
    </item>
    <item>
      <title>19 of 424 JSON-LD blocks were not JSON. Every one had JavaScript stapled to the end.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Thu, 05 Nov 2026 02:16:11 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/19-of-424-json-ld-blocks-were-not-json-every-one-had-javascript-stapled-to-the-end-ok4</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/19-of-424-json-ld-blocks-were-not-json-every-one-had-javascript-stapled-to-the-end-ok4</guid>
      <description>&lt;p&gt;I parsed 424 &lt;code&gt;application/ld+json&lt;/code&gt; blocks across 287 pages. Nineteen of them were not JSON. Each of the nineteen had a base64-encoded script stapled to the end of a perfectly valid schema object.&lt;/p&gt;

&lt;p&gt;The markup looked fine in every validator that checks fields. It only failed when something tried to actually parse it as JSON — which is the one thing a crawler does before it looks at a single field.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I pulled
&lt;/h2&gt;

&lt;p&gt;I audit a network of 114 domains. For this run, every domain got three page URLs: the homepage plus up to two non-homepage URLs taken from that domain's own root sitemap, filtered to URLs on the same host and excluding anything ending in &lt;code&gt;.xml&lt;/code&gt;, &lt;code&gt;.xml.gz&lt;/code&gt; or &lt;code&gt;.txt&lt;/code&gt;. Sitemap entries that point at other hosts or at child sitemaps are common on this network, so the filter is not optional.&lt;/p&gt;

&lt;p&gt;296 requests, 287 came back 200. Eight never answered, one returned 404. Of the 287, 239 shipped at least one &lt;code&gt;ld+json&lt;/code&gt; block. That is 83% of live pages carrying structured data, and 424 blocks in total.&lt;/p&gt;

&lt;p&gt;I did not use the Rich Results Test, and I did not trust a browser extension badge. Both answer the question "does this look like schema.org" before they answer "is this a JSON document".&lt;/p&gt;

&lt;h2&gt;
  
  
  The test is a parser, not a validator
&lt;/h2&gt;

&lt;p&gt;Extraction is a regex over the raw HTML, because &lt;code&gt;ld+json&lt;/code&gt; blocks are data, not DOM. Then &lt;code&gt;json.loads&lt;/code&gt; with no repair step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;LD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;script[^&amp;gt;]+type\s*=\s*[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']application/ld\+json[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\'][^&amp;gt;]*&amp;gt;(.*?)&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;LD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;broken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;broken&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  tail=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;broken&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: 405 parsed, 19 raised. That is 4.5% of blocks, spread over 19 pages — every failing page had exactly one failing block, so no page failed twice.&lt;/p&gt;

&lt;p&gt;The exception is always the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extra data: line 13 column 5 (char 405)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Extra data&lt;/code&gt; means the parser found a complete JSON value and then more bytes after it. Not a trailing comma, not an unquoted key, not an HTML entity. Valid JSON, followed by something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the something else was
&lt;/h2&gt;

&lt;p&gt;Here is the end of one of those blocks. The JSON closes at &lt;code&gt;}&lt;/code&gt;, and then the same &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag continues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;&lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aWYobmV3IERhdGUoKS5nZXRUaW1lKCk+MTc4MTk1OTk5ODMzMiljb25zb2xlLmxvZygnaHR0cHM6Ly9zbWFsbHRvb2wuZ2l0aHViLmlvLzIwMjEvMDgvMjEvZG93bmxvYWQvJywgJzYzNicpOw==&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))}();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Base64 in an &lt;code&gt;eval&lt;/code&gt; inside a script tag that claims to be structured data. Decoding it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# if(new Date().getTime()&amp;gt;1781959998332)console.log(
#   'https://smalltool.github.io/2021/08/21/download/', '636');
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three parts: a 13-digit timestamp used as a date gate, a URL, and a numeric code. The gate is in the past (June 2026), so the branch is always true now. The code differs per site: 131, 372, 636, 833, 1297, 1432, 1678.&lt;/p&gt;

&lt;p&gt;Seven distinct payloads, one per affected domain, identical in shape. On one of them the injection sits at the end of the only block on &lt;a href="https://nextgemcrypto.com/content/best-crypto-to-buy-2026/" rel="noopener noreferrer"&gt;a crypto buying guide&lt;/a&gt;, so that page now has no structured data at all, while the visible article is unchanged. Nineteen pages total. The set of affected hosts is a contiguous-looking slice of the network: the pages that failed are not scattered, they are all the sampled pages on those seven domains. 19 of 19 on the affected sites, 0 of 268 everywhere else.&lt;/p&gt;

&lt;p&gt;That is what a template-level insert looks like, not a per-page mistake. Something wrote one string into one layout and shipped it to every page built from that layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  It never runs. That is the interesting part
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; element with &lt;code&gt;type="application/ld+json"&lt;/code&gt; is a data block. The browser stores its text and does not compile it. So the injected code, sitting inside that tag, never executes.&lt;/p&gt;

&lt;p&gt;I did not want to assert that from memory, so I ran two files through a real headless Chrome:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- a.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__ran&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ldjson&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- b.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__ran&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plainjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both files then write &lt;code&gt;window.__ran&lt;/code&gt; into the DOM. Dumped after load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a.html -&amp;gt; FLAG=undefined
b.html -&amp;gt; FLAG=plainjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same statement, same page, different &lt;code&gt;type&lt;/code&gt;. In the data block it does nothing at all. The payload is inert. Whatever it was built to promote, it does not reach a browser, and it has not since it was inserted.&lt;/p&gt;

&lt;p&gt;Which is worse than it sounds, because it means nobody noticed. A payload that fired would have left console output and probably a bug report. This one produced silence on the page and a JSON parse error only on the crawler side, in a file nobody opens.&lt;/p&gt;

&lt;p&gt;The visible cost is intact markup that quietly stops being markup. On 17 of the 19 pages, the broken block was the page's only block, so those pages now carry zero usable structured data — no &lt;code&gt;WebSite&lt;/code&gt;, no &lt;code&gt;BreadcrumbList&lt;/code&gt;, nothing. The other two pages had a second, clean block that survived.&lt;/p&gt;

&lt;p&gt;For an affected page like &lt;a href="https://howtobuyspacexstock.com/about/" rel="noopener noreferrer"&gt;one stock-research site's about page&lt;/a&gt;, the HTML shipped to Google is 8.4 KB with a &lt;code&gt;WebSite&lt;/code&gt; entity that the parser throws away before reading &lt;code&gt;name&lt;/code&gt; or &lt;code&gt;url&lt;/code&gt;. The page renders normally. The markup does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other 56 pages, which never had a shot
&lt;/h2&gt;

&lt;p&gt;The injection is a good story because it is dramatic. It is also 19 pages out of 239. The quieter number is larger.&lt;/p&gt;

&lt;p&gt;On a separate sample — 32 domains, 86 pages that answered 200 — 70 pages shipped &lt;code&gt;ld+json&lt;/code&gt; and 16 shipped none at all. Of those 70, &lt;strong&gt;56 (80%) contained no &lt;code&gt;Article&lt;/code&gt;, &lt;code&gt;NewsArticle&lt;/code&gt;, or &lt;code&gt;BlogPosting&lt;/code&gt; node anywhere&lt;/strong&gt;. What they had was site chrome: &lt;code&gt;Organization&lt;/code&gt;, &lt;code&gt;WebSite&lt;/code&gt;, &lt;code&gt;BreadcrumbList&lt;/code&gt;, &lt;code&gt;CollectionPage&lt;/code&gt;, &lt;code&gt;ItemList&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fifty-four &lt;code&gt;ListItem&lt;/code&gt; nodes, fifty &lt;code&gt;BreadcrumbList&lt;/code&gt;, forty-two &lt;code&gt;Organization&lt;/code&gt;. Fourteen &lt;code&gt;Article&lt;/code&gt;. These are content pages — guides, comparisons, forecasts — and four out of five of them tell a crawler what the site is called but not what the page is about.&lt;/p&gt;

&lt;p&gt;The same sample had 7 of 14 &lt;code&gt;Article&lt;/code&gt; nodes with no &lt;code&gt;author&lt;/code&gt; property, and 7 nodes with no &lt;code&gt;@type&lt;/code&gt; at all, which makes them invisible regardless of what else they contain.&lt;/p&gt;

&lt;p&gt;Zero cross-host &lt;code&gt;@id&lt;/code&gt; leaks, zero future &lt;code&gt;datePublished&lt;/code&gt;, zero duplicate &lt;code&gt;@id&lt;/code&gt; within a page. The errors are not exotic. They are missing fields and one injected string.&lt;/p&gt;

&lt;p&gt;For contrast, &lt;a href="https://marketpredictionnews.com/comparisons/binary-markets-vs-multi-outcome-markets/" rel="noopener noreferrer"&gt;this comparison page&lt;/a&gt; ships four blocks with &lt;code&gt;Article&lt;/code&gt;, &lt;code&gt;BreadcrumbList&lt;/code&gt; and &lt;code&gt;FAQPage&lt;/code&gt; and parses clean. It is one of the fourteen. That is the whole difference between the 14 and the 56: someone wrote the page-level entity, and on the other pages nobody did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check I now run on every deploy
&lt;/h2&gt;

&lt;p&gt;Three questions, in this order. If the first one fails, the rest are noise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_broken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executable_tail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;article&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_author&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;atob\s*\(|eval\s*\(|;!function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executable_tail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_broken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;          &lt;span class="c1"&gt;# yields every dict in the tree
&lt;/span&gt;            &lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Article&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;article&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_author&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of those counters are not about schema.org at all. &lt;code&gt;executable_tail&lt;/code&gt; catches a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; that has stopped being a data block and started being a hole. &lt;code&gt;json_broken&lt;/code&gt; catches the case where a build step concatenated a string into a JSON payload and production never noticed, because production renders fine.&lt;/p&gt;

&lt;p&gt;A structured-data validator that starts from a parsed object can never report either of them. It only sees the blocks that already survived.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;p&gt;The deploy check now fails the build if any &lt;code&gt;ld+json&lt;/code&gt; block raises &lt;code&gt;JSONDecodeError&lt;/code&gt;, and separately greps the last 400 characters of every block for &lt;code&gt;atob(&lt;/code&gt; / &lt;code&gt;eval(&lt;/code&gt; / &lt;code&gt;;!function&lt;/code&gt;. The second check exists because the first one is a symptom: a block that fails to parse is broken today, but a block that parses and contains a live &lt;code&gt;eval&lt;/code&gt; would be worse and would pass.&lt;/p&gt;

&lt;p&gt;The seven domains are queued for a template rebuild, and the payload is being pulled at the layout level rather than page by page, since that is where it came from.&lt;/p&gt;

&lt;p&gt;The 56 pages with site-only schema are a bigger job and a slower one. That is not a bug you fix by adding a tag. It is a decision that every page type needs a page-level entity, made once, applied in the generator.&lt;/p&gt;

&lt;p&gt;The number I will keep watching is not 19. It is 56 — because 19 was loud once I looked, and 56 would have stayed quiet forever.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>seo</category>
      <category>security</category>
    </item>
    <item>
      <title>15 pages declared lang="en". 75-98% of their characters were Han.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Wed, 04 Nov 2026 02:15:23 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/15-pages-declared-langen-75-98-of-their-characters-were-han-4o19</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/15-pages-declared-langen-75-98-of-their-characters-were-han-4o19</guid>
      <description>&lt;p&gt;Fifteen pages told the browser they were English. Between 75% and 98% of their characters were Han.&lt;/p&gt;

&lt;p&gt;I only found it because I stopped checking meta tags and started counting characters. The pages had titles, descriptions, canonicals, one &lt;code&gt;h1&lt;/code&gt; each, valid JSON-LD — everything the usual audit looks at. The &lt;code&gt;lang&lt;/code&gt; attribute was the one field nobody had ever verified against the body text, and on four domains it was wrong on every page I sampled.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I sampled
&lt;/h2&gt;

&lt;p&gt;I started with 40 domains. Twelve of their root sitemaps listed nothing but child sitemaps on subdomains, so there was no page URL to sample — that is a different bug and a different post. The remaining 28 domains gave me up to four page URLs each, taken from their own sitemaps and filtered to URLs on the site's own host, excluding the homepage and any &lt;code&gt;.xml&lt;/code&gt; entries. No homepage, no sitemap, no XML: real HTML pages only.&lt;/p&gt;

&lt;p&gt;That produced 106 pages. I parsed them once for structure and re-fetched 104 of them for the language pass — two timed out on the second pass.&lt;/p&gt;

&lt;p&gt;The measurement is not clever. Strip &lt;code&gt;script&lt;/code&gt;, &lt;code&gt;style&lt;/code&gt;, and &lt;code&gt;noscript&lt;/code&gt;, count Han characters, count Latin words, and take the ratio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;html.parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTMLParser&lt;/span&gt;

&lt;span class="n"&gt;HAN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[\u3400-\u4dbf\u4e00-\u9fff]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTMLParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_starttag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;script&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;style&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noscript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_endtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;script&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;style&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noscript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;language_fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;han&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HAN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;latin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[A-Za-z]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;han&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;latin&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;han&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;han&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;latin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;han_share&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;han&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;declared&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;html[^&amp;gt;]+lang=[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']([^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']+)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 10% threshold separates the two populations cleanly enough that the exact cutoff does not matter: the mislabeled pages sat at 75% and above, and the English pages sat at 0%. There is no ambiguous middle in this sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  The split
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Declared &lt;code&gt;lang&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Pages&lt;/th&gt;
&lt;th&gt;Actually Chinese (&amp;gt;10% Han)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zh-CN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;en&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fifteen of 56 English-declared pages, 26.8%, were Chinese. Put the other way: of the 63 pages in this sample whose body text is Chinese, 23.8% announced themselves as English.&lt;/p&gt;

&lt;p&gt;The distribution is not random. All fifteen sat on four domains, and on those domains every page I sampled was wrong — 4 of 4, 4 of 4, 4 of 4, 3 of 3. That is the signature of a constant, not a content mistake. Somebody wrote &lt;code&gt;&amp;lt;html lang="en"&amp;gt;&lt;/code&gt; into the root layout of a site whose content pipeline emits Chinese, and nothing downstream ever compared the two.&lt;/p&gt;

&lt;p&gt;You can see it in the wild: &lt;a href="https://cryptobullforecast.com/faq/" rel="noopener noreferrer"&gt;an FAQ page that declares &lt;code&gt;en&lt;/code&gt; while 96.7% of its characters are Han&lt;/a&gt;, and &lt;a href="https://eventmarkethub.com/category/crypto/" rel="noopener noreferrer"&gt;a category page with the same declaration and 94.3% Chinese text&lt;/a&gt;. Both render fine, both pass a head-tag audit, both would fail this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a wrong &lt;code&gt;lang&lt;/code&gt; actually breaks
&lt;/h2&gt;

&lt;p&gt;It does not break rendering, which is why it survives. It breaks the parts of the stack that read the attribute instead of the pixels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Screen readers&lt;/strong&gt; choose a voice from &lt;code&gt;lang&lt;/code&gt;. An English voice reading Han characters either spells them out or skips them, depending on the engine. This is the one failure mode with a real user on the other end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translation prompts.&lt;/strong&gt; Chrome keys its offer to translate off the declared language first. A page that says &lt;code&gt;en&lt;/code&gt; rarely gets the offer, even when every word is Chinese.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lang&lt;/code&gt;-aware CSS.&lt;/strong&gt; &lt;code&gt;:lang(zh)&lt;/code&gt; rules never match, and quote characters, hyphenation, and &lt;code&gt;text-transform&lt;/code&gt; follow English rules on Chinese text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font fallback.&lt;/strong&gt; The browser resolves fonts per script, but the declared language steers the choice within a script — and it decides whether CJK glyphs come from the page's intended font stack or from whatever the OS falls back to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search.&lt;/strong&gt; Google has said for years that it infers page language primarily from the visible content, so I am not going to claim a ranking delta from 104 pages. What I can say is that the stack had no second opinion to offer: 28 of 100 pages carried any &lt;code&gt;hreflang&lt;/code&gt; alternate, zero of 100 sent a &lt;code&gt;Content-Language&lt;/code&gt; header, and all fifteen mislabeled pages carried zero &lt;code&gt;hreflang&lt;/code&gt;. Nothing in the markup disagreed with the wrong tag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The control case is boring, which is the point: &lt;a href="https://marketforecastdaily.com/faq/" rel="noopener noreferrer"&gt;the same FAQ template on another domain in the same network&lt;/a&gt; declares &lt;code&gt;en&lt;/code&gt; and is 0% Han. Same generator, correct locale, because that site's config was set correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check, as a CI gate
&lt;/h2&gt;

&lt;p&gt;I turned this into a script that crawls a sample and exits non-zero. It is thirty lines and it would have caught all fifteen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;language_fingerprint&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;language_fingerprint&lt;/span&gt;  &lt;span class="c1"&gt;# the function above
&lt;/span&gt;
&lt;span class="n"&gt;URLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;  &lt;span class="c1"&gt;# one page per template, not every page
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lang-guard/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;language_fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;declared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;declared&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;share&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;han_share&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;implied&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;share&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;declared&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;implied&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ok  &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; declared=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;declared&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;implied=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;implied&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; han=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;share&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;

&lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;URLS&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it on one representative page per template — not per URL. The failure I found was template-level, so a four-URL sample per site was enough to catch it on four sites out of 28.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is one line, the source of truth is the hard part
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;html lang="en"&amp;gt;&lt;/code&gt; in a root layout is a hardcoded default pretending to be a decision. The page's language belongs to the content, or to the locale segment of the route, and the layout should read it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// __root.tsx — one locale, resolved per request, never a literal&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Root&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If articles carry their own language column, use that instead and let the route default be a fallback that logs when it fires. A fallback that fires silently is how you end up with a site whose Chinese content announces itself as English for as long as nobody counts characters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more things the same crawl turned up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;2,777 leaked source annotations.&lt;/strong&gt; Seven pages across two domains shipped attributes like &lt;code&gt;data-tsd-source="/src/components/Layout.tsx:10:9"&lt;/code&gt; — 2,777 of them in total. That is TanStack Start's dev-mode source annotation surviving into production HTML. It costs users nothing to download and it hands anyone who views source your component tree, file names, and line numbers. If you see it, your production build is running with the dev transform on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero &lt;code&gt;Content-Language&lt;/code&gt; headers.&lt;/strong&gt; Not one of the 100 pages that answered sent the header. That is normal for most static stacks and not a bug on its own — but combined with a wrong &lt;code&gt;lang&lt;/code&gt;, it means the response carried no language signal that was correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not prove
&lt;/h2&gt;

&lt;p&gt;Four pages per domain, one crawl, one afternoon. The Han-ratio heuristic ignores mixed-script pages, and I dropped four URLs that failed on re-fetch rather than retrying them. I have no before-and-after data on indexing or rankings, because I changed nothing — this was a measurement pass, and the fix is not shipped yet.&lt;/p&gt;

&lt;p&gt;What it does establish is narrower and more useful: a head-tag audit says nothing about whether your &lt;code&gt;lang&lt;/code&gt; attribute is true, and on a 28-site sample, 15 pages were lying about it in the same direction, from the same root layout, on four sites that had never checked.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>python</category>
      <category>a11y</category>
    </item>
    <item>
      <title>My sitemap listed 1,926 entries. Every one was a subdomain.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Tue, 03 Nov 2026 02:10:15 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/my-sitemap-listed-1926-entries-every-one-was-a-subdomain-3m60</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/my-sitemap-listed-1926-entries-every-one-was-a-subdomain-3m60</guid>
      <description>&lt;p&gt;I expected 23 of my 119 sites to be underperformers. I did not expect Google to have zero of their real pages in the index.&lt;/p&gt;

&lt;p&gt;I sampled 15 URLs per property through the Search Console URL Inspection API and grouped the results. On 91 properties the sample came back with apex URLs, and 713 of 1,346 sampled URLs were &lt;code&gt;Submitted and indexed&lt;/code&gt;: a 53.0% hit rate. On 23 properties the sample returned &lt;strong&gt;no apex URLs at all&lt;/strong&gt;. Every URL Google knew about for those domains, 2,104 of them, sat on a subdomain.&lt;/p&gt;

&lt;p&gt;Those subdomains were mine. I just never meant to publish them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement
&lt;/h2&gt;

&lt;p&gt;The check is small enough to run in a loop. For each property, ask the inspection API for a sample of URLs and bucket each result by whether its host matches the property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inspect_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://searchconsole.googleapis.com/v1/urlInspection/index:inspect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inspectionUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;siteUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sc-domain:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;same&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;foreign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;same_host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;same&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;foreign_host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foreign&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;indexed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;same&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The split is blunt: 91 properties had a mix leaning toward apex URLs. Twenty-three had &lt;code&gt;same_host: 0&lt;/code&gt; and a &lt;code&gt;foreign_host&lt;/code&gt; count in the dozens or low hundreds. Two more were lopsided rather than empty, with 62 apex URLs against 250 subdomain URLs each.&lt;/p&gt;

&lt;p&gt;That last group is the tell. A property with 62 apex URLs and 250 subdomain URLs is not a property with a crawling problem. It is a property where something else is competing for the same index space, and winning.&lt;/p&gt;

&lt;p&gt;The pages that lost that competition are the ones I actually maintain. These are not thin domains with nothing to index: they carry the same kind of work as the properties that sampled cleanly. On a healthy property that work does show up in the sample. &lt;code&gt;predictionguidecn.com&lt;/code&gt; returned 183 apex URLs with 11 of its 15 sampled pages &lt;code&gt;Submitted and indexed&lt;/code&gt;; &lt;code&gt;eventmarketcn.com&lt;/code&gt; returned 145 apex URLs with 10 of 15 indexed. Content of that shape includes things like the encyclopedia entry on &lt;a href="https://predictionguidecn.com/wiki/liquidity/" rel="noopener noreferrer"&gt;how liquidity is priced in prediction markets&lt;/a&gt;, the comparison of &lt;a href="https://eventmarketcn.com/polymarket/vs-kalshi/" rel="noopener noreferrer"&gt;how Polymarket and Kalshi differ on fees and settlement&lt;/a&gt;, and a year of &lt;a href="https://markettrendlab.com/category/crypto/" rel="noopener noreferrer"&gt;crypto market trend archives&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;On the 23 properties, the same sampling loop came back empty. Same content pipeline, same templates, no apex URLs in the index at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The subdomains were alive, and they claimed themselves
&lt;/h2&gt;

&lt;p&gt;Before assuming a DNS wildcard was pointing at a parking page, I fetched a few. 21 of the 23 responded &lt;code&gt;200&lt;/code&gt;. All 21 declared &lt;code&gt;index, follow&lt;/code&gt; in their robots meta. And all 21 pointed their canonical at themselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://a7a5.altcoinfundingdesk.com/
  status    : 200
  canonical : https://a7a5.altcoinfundingdesk.com/
  robots    : index,follow,max-image-preview:large
  title     : A7A5 资金费率事件怎么复盘？

https://altcoinfundingdesk.com/
  status    : 200
  canonical : https://altcoinfundingdesk.com/
  robots    : index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are correct by their own rules. Neither knows the other exists. That is the whole failure: two URLs, same publishing system, two independent canonical claims, no cross-reference. Google has to pick one, and on 23 properties it picked the subdomain.&lt;/p&gt;

&lt;p&gt;Twenty-one out of twenty-one is not a random sample. When every single host self-canonicalizes, the canonical tag is not expressing an editorial decision. It is expressing whatever &lt;code&gt;request.headers.host&lt;/code&gt; happened to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Google went looking there
&lt;/h2&gt;

&lt;p&gt;A canonical tag does not create discovery. Something has to link to those subdomains first, or submit them.&lt;/p&gt;

&lt;p&gt;The apex &lt;code&gt;sitemap.xml&lt;/code&gt; was doing it.&lt;/p&gt;

&lt;p&gt;I fetched the sitemap from eight of the affected domains and counted &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; entries of each shape. 1,926 entries in total. Every one pointed at a subdomain sitemap. Zero pointed at a page on the domain that served the file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cryptochartshub.com       loc=256    subdomain=256    apex=0
altcoinfundingdesk.com    loc=118    subdomain=118    apex=0
dexguidehub.com           loc=278    subdomain=278    apex=0
tokenmarginlab.com        loc=256    subdomain=256    apex=0
perpfundingwatch.com      loc=256    subdomain=256    apex=0
coinwikicenter.com        loc=256    subdomain=256    apex=0
web3academyhub.com        loc=256    subdomain=256    apex=0
exchangeguidehub.com      loc=250    subdomain=250    apex=0
--------------------------------------------------------------
total                     loc=1926   subdomain=1926   apex=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;100.0% subdomain, 0.0% apex. The file named &lt;code&gt;sitemap.xml&lt;/code&gt; on &lt;code&gt;cryptochartshub.com&lt;/code&gt; was a sitemap index listing 256 per-token microsites, and not one entry for &lt;code&gt;cryptochartshub.com&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;So the sequence was not mysterious. The sitemap told Google "here are 256 sites", Google crawled them, each one said &lt;code&gt;index, follow&lt;/code&gt; and claimed itself as canonical, and the apex pages were never submitted at all. No contradiction existed for Google to resolve, because nothing ever told it the apex mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  One config value doing two jobs
&lt;/h2&gt;

&lt;p&gt;The generator reads a content registry: one record per token, one per site. The registry is the source of truth for what to build. Somewhere along the way it also became the source of truth for what to submit.&lt;/p&gt;

&lt;p&gt;The sitemap builder walked the registry and emitted every host it found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_sitemap_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# one loc per host the content registry mentions
&lt;/span&gt;    &lt;span class="n"&gt;locs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sitemap.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the canonical helper did the same thing one layer down, reading the host off the request instead of off the config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines, same assumption: that "the host this code is running on" and "the host this content belongs to" are the same value. They are the same value in production with one deployment and one domain. They stop being the same value the moment a registry has more than one host in it, which is exactly the situation the registry exists to describe.&lt;/p&gt;

&lt;p&gt;The fix is not a canonical tag on the subdomains. It is making the canonical host an explicit configuration value and deriving both the sitemap and the canonical tag from that value alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PRIMARY_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;primary_host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="c1"&gt;# e.g. "cryptochartshub.com"
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canonical_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PRIMARY_HOST&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_sitemap_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# only the site's own sitemaps, never the registry's other hosts
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render_index&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PRIMARY_HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sitemap-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                         &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sitemap_shards&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;guard_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;PRIMARY_HOST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PRIMARY_HOST&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three changes, and they have to ship together. Fixing the sitemap alone leaves the subdomains reachable and self-canonical. Fixing the canonical alone leaves the sitemap advertising 256 hosts. The redirect is what actually retires the duplicates, because a &lt;code&gt;301&lt;/code&gt; from the subdomain to the apex is the only signal that says "these were never separate things".&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check your own site in two minutes
&lt;/h2&gt;

&lt;p&gt;You do not need Search Console to catch this. The sitemap check is a single request per domain, and it finds the problem before the index does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit_sitemap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sitemap.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (compatible; sitemap-audit/1.0)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;locs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;loc&amp;gt;([^&amp;lt;]+)&amp;lt;/loc&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;locs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;apex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hosts&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sub&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hosts&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;apex&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;apex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subdomain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;foreign&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cryptochartshub.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dexguidehub.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web3academyhub.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;audit_sitemap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it against your own domain. If &lt;code&gt;apex&lt;/code&gt; is 0 and &lt;code&gt;locs&lt;/code&gt; is not, you have the same bug, and it has been running for as long as the registry has had more than one host in it. If &lt;code&gt;foreign&lt;/code&gt; is non-zero, someone else's host is in your sitemap, which is a different and more urgent problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;The index damage is not repaired by fixing the code, and I am not going to pretend otherwise. I have not finished the rollout, so I have no recovery number to report. Consolidating duplicate hosts takes weeks, not days. The honest measurement window is four to six weeks, and the metric to watch is &lt;code&gt;coverageState&lt;/code&gt; in the URL Inspection response, not raw impressions, which bounce around for unrelated reasons.&lt;/p&gt;

&lt;p&gt;Two things I would keep from this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat the host as a config value, not a runtime fact.&lt;/strong&gt; Any string built from &lt;code&gt;request.headers.host&lt;/code&gt; that ends up in a canonical, an &lt;code&gt;og:url&lt;/code&gt;, a sitemap, or a redirect target is a bug waiting for a second deployment target. There is no version of this that stays correct when the same code serves two hostnames.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit the shape of your sitemap, not just its size.&lt;/strong&gt; Every SEO check I had counted URLs and checked status codes. All of them passed, on a sitemap where 100% of the entries belonged to somebody else. A sitemap index that is 100% subdomains looks perfectly healthy to any tool that only counts &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; elements and fetches them.&lt;/p&gt;

&lt;p&gt;The 2,104 subdomain URLs were not spam, not a hijack, and not an attack. They were my own build output, deployed exactly where the config told it to go, competing with the pages I wanted indexed. That is the part that took longest to accept: nothing was broken in the sense of returning an error. It was all working, and pointing at the wrong host.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>python</category>
      <category>devops</category>
    </item>
    <item>
      <title>38.8% to 58.5% indexed in 15 days. Five sites out of 45 did half of it.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Mon, 02 Nov 2026 02:32:56 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/388-to-585-indexed-in-15-days-five-sites-out-of-45-did-half-of-it-5150</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/388-to-585-indexed-in-15-days-five-sites-out-of-45-did-half-of-it-5150</guid>
      <description>&lt;p&gt;Submitted-and-indexed went from &lt;strong&gt;164 to 247&lt;/strong&gt; on the same 524 URLs. That is 38.8% → 58.5% in fifteen days, the best number this fleet has produced all year.&lt;/p&gt;

&lt;p&gt;Then I grouped the same rows by site. Nineteen of 45 sites moved up, 24 did not move at all, and two went down. The five biggest movers produced &lt;strong&gt;43 of the 85 newly indexed URLs — 50.6% of the gain&lt;/strong&gt; — while eight sites still had zero of ten sampled URLs indexed.&lt;/p&gt;

&lt;p&gt;The aggregate was real. It was also a story about five sites that the other forty were invited to.&lt;/p&gt;

&lt;p&gt;This post covers what I measured, the two hypotheses that failed, and the instrumentation bug that had been hiding a fifth of the network from every report I ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sample, and what it is not
&lt;/h2&gt;

&lt;p&gt;Forty-five sites, ten sitemap URLs each, inspected through the Search Console URL Inspection API on 2026-10-15 and again on 2026-10-30. Same 524 URLs both times — intersection 524, zero added, zero dropped. A fixed panel, so the movement below is genuine URL-level state change rather than sampling churn.&lt;/p&gt;

&lt;p&gt;What it is not: a census. The fleet serves &lt;strong&gt;67,543 HTML pages&lt;/strong&gt; across 119 sites, so 524 URLs is 0.8% of it, and the panel is capped at ten per site on sites with a median of 243 pages. Every number after this point is a sample statistic. I keep saying that because the last time I forgot, I shipped a slide that said "1.5% redirect rate" about two broken domains.&lt;/p&gt;

&lt;p&gt;The pages in the panel are mostly long-form explainers — the kind of thing a site keeps under &lt;code&gt;/learn/&lt;/code&gt;: &lt;a href="https://predictmarketcn.com/learn/how-to-read-odds/" rel="noopener noreferrer"&gt;how to read a prediction-market price as a signal&lt;/a&gt;, &lt;a href="https://eventmarketcn.com/learn/kelly-staking/" rel="noopener noreferrer"&gt;Kelly sizing for binary markets&lt;/a&gt;, macro-indicator glossaries. Boring, stable, cheap to produce. That matters later.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fifth of the panel could never be measured
&lt;/h2&gt;

&lt;p&gt;102 of 524 inspections came back unusable. Not "no data" — an actual error, &lt;code&gt;API_ERROR_403&lt;/code&gt;, with the message every Search Console user eventually meets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"You do not own this site, or the inspected URL is not part of this property."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URL is fine. The property is wrong. Twenty-eight of the 119 sites in this fleet are multi-root: one apex plus up to &lt;strong&gt;280 subdomains&lt;/strong&gt; (&lt;code&gt;aave.coinatlaslab.com&lt;/code&gt;, &lt;code&gt;apenft.altcoinfundingdesk.com&lt;/code&gt;, and 5,700 more across the network). The Search Console property is registered on the apex. Pages served from &lt;code&gt;aave.coinatlaslab.com&lt;/code&gt; are therefore outside every property I own, which is technically correct and operationally useless: the sitemap lists them, Google may well index them, and no coverage report will ever show either fact.&lt;/p&gt;

&lt;p&gt;Of the 112 subdomain URLs in the panel, &lt;strong&gt;101 returned 403 — 90%&lt;/strong&gt;. They span 16 domains. The one non-403 error was a 500 on a retry-after-three-attempts host.&lt;/p&gt;

&lt;p&gt;The obvious fix is a property per subdomain, and it does not scale: 5,739 roots would need 5,739 properties. The cheaper fix is to stop putting subdomain URLs in an apex sitemap you cannot report on, or to front them with a canonical to an apex URL you can. Either way, the instrumentation has to check host ownership before it trusts a coverage state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;apex | sub — sub means the property cannot inspect this URL at all.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inspectionUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;siteUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;languageCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://searchconsole.googleapis.com/v1/urlInspection/index:inspect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inspectionResult&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;indexStatusResult&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coverageState&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# 403 = not my property. Do not fold this into "not indexed".
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API_ERROR_%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important line is the comment. If you count a 403 as a coverage state, your index rate silently improves whenever you sample more subdomains, because the failures vanish from the denominator. Mine did exactly that for three weeks before I looked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually moved
&lt;/h2&gt;

&lt;p&gt;Denominator 423 → 422, so the delta is not an artifact of dropped rows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;coverageState&lt;/th&gt;
&lt;th&gt;10-15&lt;/th&gt;
&lt;th&gt;10-30&lt;/th&gt;
&lt;th&gt;change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Submitted and indexed&lt;/td&gt;
&lt;td&gt;164 (38.8%)&lt;/td&gt;
&lt;td&gt;247 (58.5%)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+19.8 pt&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discovered – not indexed&lt;/td&gt;
&lt;td&gt;160 (37.8%)&lt;/td&gt;
&lt;td&gt;82 (19.4%)&lt;/td&gt;
&lt;td&gt;−18.4 pt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL unknown to Google&lt;/td&gt;
&lt;td&gt;94 (22.2%)&lt;/td&gt;
&lt;td&gt;84 (19.9%)&lt;/td&gt;
&lt;td&gt;−2.3 pt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crawled – not indexed&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;−0.2 pt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soft 404&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;+0.5 pt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page with redirect&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;+0.5 pt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate, no user canonical&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;+0.7 pt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;150 of 524 URLs changed state. The forward bucket is the one you want: 67 went &lt;code&gt;Discovered → Indexed&lt;/code&gt;, 17 went &lt;code&gt;Unknown → Indexed&lt;/code&gt;, 1 came back from &lt;code&gt;Crawled&lt;/code&gt;. The backward bucket is the one the dashboard hides: &lt;strong&gt;33 URLs went &lt;code&gt;Discovered → Unknown&lt;/code&gt;&lt;/strong&gt;, meaning Google dropped even the queued-crawl record it previously had. Add the new soft 404s, redirects and duplicates and 42 of the 150 transitions were regressions.&lt;/p&gt;

&lt;p&gt;A network-level "+20 points" and "42 URLs quietly lost their place in the crawl queue" are the same dataset. Only one of them fits on a chart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five sites did half of it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;per_site&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prop_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;site&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prop_key&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prop_key&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coverage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Submitted and indexed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;per_site&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows_1015&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;per_site&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows_1030&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;gain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# 85
&lt;/span&gt;&lt;span class="n"&gt;top5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top5&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                     &lt;span class="c1"&gt;# 0.506
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;        &lt;span class="c1"&gt;# 24 sites, unmoved
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nineteen sites up, 24 flat, 2 down. Gross gain 85, gross loss 2, net 83. Half the gain sits in five domains: &lt;code&gt;predictmarketcn.com&lt;/code&gt; 0→10, &lt;code&gt;eventmarketcn.com&lt;/code&gt; 0→9, &lt;code&gt;predictionlabcn.com&lt;/code&gt; 0→9, &lt;code&gt;marketforecastcenter.com&lt;/code&gt; 1→9, &lt;code&gt;marketpredictionnow.com&lt;/code&gt; 0→7.&lt;/p&gt;

&lt;p&gt;Now the part I cannot explain well. Those five sites share a template, a deploy pipeline, and a sitemap generator with the eight sites that stayed at zero. &lt;code&gt;markettrendlab.com&lt;/code&gt; publishes &lt;strong&gt;1,111 sitemap URLs&lt;/strong&gt; — a &lt;a href="https://markettrendlab.com/trends/btc-deep-analysis-2026/" rel="noopener noreferrer"&gt;per-coin technical and fundamental analysis archive&lt;/a&gt; — and indexed 0 of 10 sampled pages in both rounds. &lt;code&gt;coinatlaslab.com&lt;/code&gt; has 1,612 pages and 0 of 10. &lt;code&gt;predictioncn.com&lt;/code&gt;, &lt;code&gt;predictionwikicn.com&lt;/code&gt;, &lt;code&gt;marketoraclecenter.com&lt;/code&gt;, &lt;code&gt;eventmarkethub.com&lt;/code&gt;, &lt;code&gt;coinwikipro.com&lt;/code&gt;: also 0 of 10.&lt;/p&gt;

&lt;p&gt;Same code, same host, same day. If your model of indexing is "publish, ping sitemap, wait", this table is telling you the model is missing a variable. I have not isolated it yet, and I am not going to pretend a fifteen-day window with one measurement per URL did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hypothesis that failed
&lt;/h2&gt;

&lt;p&gt;The fleet-wide crawl I ran the same morning gave me a clean suspect: &lt;strong&gt;915 of 67,543 pages (1.35%) had zero internal inlinks&lt;/strong&gt;. 109 of 119 sites had at least one orphan, 16 sites were 100% orphaned, and orphan pages are exactly the ones that sit in &lt;code&gt;Discovered&lt;/code&gt; forever. It should have predicted the index rate.&lt;/p&gt;

&lt;p&gt;It did not. On the 45 sites in the panel, the correlation between orphan ratio and indexed ratio is &lt;strong&gt;r = 0.034&lt;/strong&gt; — indistinguishable from noise.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;site&lt;/th&gt;
&lt;th&gt;orphan ratio&lt;/th&gt;
&lt;th&gt;indexed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;marketoracledaily.com&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;1/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;coinatlaslab.com&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;0/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;polymarkettutorials.com&lt;/td&gt;
&lt;td&gt;4.84%&lt;/td&gt;
&lt;td&gt;0/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;predictionwikicn.com&lt;/td&gt;
&lt;td&gt;7.03%&lt;/td&gt;
&lt;td&gt;0/10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two worst orphan ratios in the fleet have the same index rate as two sites with none. A 1.35% orphan rate is simply too small to move a ten-URL sample: you would need the orphans to be sampled, and at ten per site they mostly are not. I fixed nothing here, and the honest conclusion is that the orphan census was measuring the wrong population for this question — it is a link-graph health metric, not an indexing predictor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second experiment, which is less flattering
&lt;/h2&gt;

&lt;p&gt;On 2026-10-16 I shipped server-rendered static output for eight single-page-app sites, so that crawlers would get real HTML instead of an empty &lt;code&gt;&amp;lt;div id="root"&amp;gt;&lt;/code&gt;. Fourteen days later I sampled 15 routes per site — 120 URLs, deterministic seed, drawn from the manifest of routes that were actually static-rendered:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;th&gt;count&lt;/th&gt;
&lt;th&gt;share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Submitted and indexed&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;30.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL unknown to Google&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;39.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discovered – not indexed&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;20.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate / canonical conflicts&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;8.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other (redirect, alternate canonical)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Per site: &lt;code&gt;coinpriceoracle.com&lt;/code&gt; 13/15, &lt;code&gt;coinforecastpro.com&lt;/code&gt; 8/15, then &lt;code&gt;bullrunalpha.com&lt;/code&gt;, &lt;code&gt;cryptobullforecast.com&lt;/code&gt;, &lt;code&gt;nextaltcoinhub.com&lt;/code&gt; at 5/15, and &lt;strong&gt;three sites at 0/15&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Thirty percent after two weeks is not a failure — new routes need discovery time, and the pre-change baseline for these same routes was not measured, which is the mistake I would undo first. But 39% of freshly rendered URLs are still &lt;em&gt;unknown to Google&lt;/em&gt;, not "discovered, waiting". Google has no record of them at all. That is a discovery problem, not a quality problem, and no amount of on-page work fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in the instrumentation
&lt;/h2&gt;

&lt;p&gt;Four rules, all of which cost me a wrong report first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Report per-site distribution, never the fleet average.&lt;/strong&gt; &lt;code&gt;19 up / 24 flat / 2 down&lt;/code&gt; next to &lt;code&gt;+19.8 pt&lt;/code&gt; is the whole story; either one alone is misinformation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate transport and permission errors from coverage states.&lt;/strong&gt; A 403 is missing instrumentation, not a bad page. Keep it out of the denominator and print it as its own line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot the same URL panel.&lt;/strong&gt; 524 URLs, identical both runs, made a 150-row transition matrix possible. Random sampling each time would have produced a plausible number and no explanation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the baseline before shipping the fix.&lt;/strong&gt; I did this for the panel and did not do it for the static-rendering rollout, so the most interesting change in the quarter has no before-row.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fleet now sits at 58.5% indexed on the panel, with a fifth of its URLs structurally unmeasurable and a discovery gap on the newest pages that no dashboard will show me. Next window I am going after the 33 URLs that fell out of the crawl queue — those are the ones with an answer in them.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>python</category>
      <category>webdev</category>
      <category>analytics</category>
    </item>
    <item>
      <title>A 1.5% redirect rate looked fine. One site's sitemap was 92% redirects.</title>
      <dc:creator>tong zhang</dc:creator>
      <pubDate>Thu, 29 Oct 2026 02:09:25 +0000</pubDate>
      <link>https://dev.to/tong_zhang_142dabd4f2b865/a-15-redirect-rate-looked-fine-one-sites-sitemap-was-92-redirects-47nk</link>
      <guid>https://dev.to/tong_zhang_142dabd4f2b865/a-15-redirect-rate-looked-fine-one-sites-sitemap-was-92-redirects-47nk</guid>
      <description>&lt;p&gt;I probed 1,089 sitemap URLs across 119 sites with redirects switched off. 1,062 came back &lt;code&gt;200&lt;/code&gt;. Sixteen came back &lt;code&gt;301&lt;/code&gt;. That is a 1.5% redirect rate, which reads like a rounding error — the kind of number you put in a slide and move on from.&lt;/p&gt;

&lt;p&gt;Then I grouped the same results by domain. Two sites accounted for all sixteen: one at &lt;strong&gt;11 of 12 (92%)&lt;/strong&gt;, one at &lt;strong&gt;5 of 12 (42%)&lt;/strong&gt;. The other 117 sites contributed zero. The aggregate number was not a measurement of the fleet. It was 1,062 green lights diluting two red ones.&lt;/p&gt;

&lt;p&gt;That is the bug this post is about, and it is not a redirect bug. The redirects were correct and intentional. The bug was that my sitemap validator — and probably yours — had been reporting on an XML file for months and calling it a verdict on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the validator actually checks
&lt;/h2&gt;

&lt;p&gt;Before touching HTTP, I ran the string layer over all 119 sitemaps: &lt;strong&gt;31,838 URLs&lt;/strong&gt;, checking for duplicate &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; entries, trailing-slash duplicates, illegal percent-encoding, and cross-host URLs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;check&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;exact duplicates&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;trailing-slash duplicate pairs&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;illegal encoding&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cross-host URLs&lt;/td&gt;
&lt;td&gt;4,485 in 25 sites&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Format-wise, the fleet was clean. Zero duplicates, zero encoding faults, and the sitemap parsed as valid XML in every case. If your CI asserts on sitemap shape — and mine did — this run passed.&lt;/p&gt;

&lt;p&gt;Four thousand four hundred and eighty-five URLs were pointing at hosts that are not the site that submitted them, which is its own problem, but the &lt;em&gt;format&lt;/em&gt; was flawless. A document can be perfectly well-formed and still describe URLs that the server refuses to serve. Nothing in the XML tells you which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probe the server, not the file
&lt;/h2&gt;

&lt;p&gt;The probe has to do one thing the validator does not: refuse to follow redirects. &lt;code&gt;curl&lt;/code&gt; without &lt;code&gt;-L&lt;/code&gt;, &lt;code&gt;requests&lt;/code&gt; with &lt;code&gt;allow_redirects=False&lt;/code&gt;, or in bare stdlib, a redirect handler that returns &lt;code&gt;None&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cf&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;

&lt;span class="n"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/141.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NoRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPRedirectHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;redirect_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;                      &lt;span class="c1"&gt;# hand the 3xx back instead of chasing it
&lt;/span&gt;
&lt;span class="n"&gt;OPENER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_opener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NoRedirect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return (status, Location). status is None on transport failure.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes=0-0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;OPENER&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# 3xx and 4xx arrive here
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;     &lt;span class="c1"&gt;# do NOT count these as failures yet
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Range: bytes=0-0&lt;/code&gt; keeps the transfer at a byte; you are asking for a status line, not a document.&lt;/p&gt;

&lt;p&gt;That last line matters more than it looks. On this run, 11 URLs came back as transport errors, and every single one returned &lt;code&gt;200&lt;/code&gt; when retried with &lt;code&gt;curl&lt;/code&gt;. Python's TLS stack was throwing &lt;code&gt;record layer failure&lt;/code&gt; on hosts that were perfectly healthy — confirmed by those hosts having normal impressions in Search Console the same week, and by the second opinion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;probe_curl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-sS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/dev/null&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%{http_code}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--max-redirs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never let a language's HTTP client be the final word on whether a host is up. A &lt;code&gt;None&lt;/code&gt; means "my client failed", not "their server failed". Those are different findings and they go in different columns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The aggregate that hid everything
&lt;/h2&gt;

&lt;p&gt;The number that matters is not the fleet-wide rate. It is the per-site rate, with a hard threshold, because trailing-slash breakage is not distributed — it is all-or-nothing per deploy configuration. A site either normalizes to slash or it does not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locs_by_site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;locs_by_site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fut&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fut&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;307&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;308&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="c1"&gt;# a redirect that only differs by the trailing slash is a
&lt;/span&gt;                &lt;span class="c1"&gt;# canonicalisation rule firing, not a URL migration
&lt;/span&gt;                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_sample&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_sample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;loc.rstrip("/") == url.rstrip("/")&lt;/code&gt; comparison is the whole diagnosis in one line. It separates two findings that look identical in a status code column:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;slash&lt;/code&gt;&lt;/strong&gt; — the URL is alive, but under a different spelling. Your sitemap and your host disagree about canonical form. Every crawl pays a redirect hop and Google records &lt;code&gt;Page with redirect&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;moved&lt;/code&gt;&lt;/strong&gt; — the URL genuinely relocated. Real migration, different fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My first pass conflated them and reported "16 redirects" as one category. It is two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five policies behind eleven identical-looking sitemaps
&lt;/h2&gt;

&lt;p&gt;The interesting result came from narrowing to the 11 sites whose sitemap contains &lt;strong&gt;only&lt;/strong&gt; URLs without a trailing slash. If trailing slashes were a single convention, these 11 should behave alike. I fetched the first five URLs from each sitemap, hit them verbatim, then hit the opposite-slash spelling of each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;site&lt;/th&gt;
&lt;th&gt;sitemap form&lt;/th&gt;
&lt;th&gt;as-listed&lt;/th&gt;
&lt;th&gt;opposite slash&lt;/th&gt;
&lt;th&gt;reading&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;coinwikipro.com&lt;/td&gt;
&lt;td&gt;no slash&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;both spellings served — duplicate risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openaiipovaluation.com&lt;/td&gt;
&lt;td&gt;no slash&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;both spellings served&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openaipreipo.com&lt;/td&gt;
&lt;td&gt;no slash&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;both spellings served&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openaistocknews.com&lt;/td&gt;
&lt;td&gt;no slash&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;both spellings served&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cryptobullforecast.com&lt;/td&gt;
&lt;td&gt;no slash&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;301&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;canonicalises &lt;strong&gt;to&lt;/strong&gt; slash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;forecastmarketcn.com&lt;/td&gt;
&lt;td&gt;no slash&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;301&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;canonicalises &lt;strong&gt;to&lt;/strong&gt; no-slash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;marketsignalscenter.com&lt;/td&gt;
&lt;td&gt;slash&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;301&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;canonicalises &lt;strong&gt;to&lt;/strong&gt; slash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;marketpredictionworld.com&lt;/td&gt;
&lt;td&gt;no slash&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;403&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;opposite spelling blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;polymarkettutorials.com&lt;/td&gt;
&lt;td&gt;no slash&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;404&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;static files, no directory index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;yesno-market.com&lt;/td&gt;
&lt;td&gt;no slash&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;404&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;static files, no directory index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openaiipostock.com&lt;/td&gt;
&lt;td&gt;no slash&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;200 ×4, 301 ×1&lt;/td&gt;
&lt;td&gt;mixed within one sitemap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five distinct policies across eleven sitemaps that are, at the XML level, byte-for-byte interchangeable in shape. A &lt;a href="https://forecastmarketcn.com/compare/polymarket-vs-kalshi" rel="noopener noreferrer"&gt;generated comparison page that canonicalises to the slashless form&lt;/a&gt; and a &lt;a href="https://yesno-market.com/basics.html" rel="noopener noreferrer"&gt;static hand-written page that has no directory index at all&lt;/a&gt; look the same to every sitemap linter ever written, and disagree about what &lt;code&gt;/foo&lt;/code&gt; means.&lt;/p&gt;

&lt;p&gt;Only one of the five policies is a defect today. And that framing is the trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sites that pass are the ones to worry about
&lt;/h2&gt;

&lt;p&gt;Four of those sites return &lt;code&gt;200&lt;/code&gt; for both spellings. That is not a correct configuration — it is a &lt;em&gt;tolerant&lt;/em&gt; one. Two URLs serve identical content, and whether Google consolidates them depends entirely on a &lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt; you may or may not have emitted. Nothing in the sitemap expresses this, and nothing in the response code flags it.&lt;/p&gt;

&lt;p&gt;The single redirecting site in the fleet is the one I'd look at as a fixed target: &lt;a href="https://cryptobullforecast.com/category/kyber-network-prediction/" rel="noopener noreferrer"&gt;its category pages are listed in the sitemap in their non-canonical form&lt;/a&gt;, so a crawler fetches a URL whose only job is to hand back a 301 pointing at the slash version. Eleven of twelve sampled URLs did exactly that.&lt;/p&gt;

&lt;p&gt;At the other end, &lt;code&gt;marketpredictionworld.com&lt;/code&gt; answers the opposite slash with &lt;strong&gt;403&lt;/strong&gt; rather than a redirect — five out of five. Not a 301, not a 404. Something in front of that origin treats the alternate spelling as forbidden, which means a single wrong link anywhere on the site produces a status code that reads like a block rather than a typo.&lt;/p&gt;

&lt;p&gt;So here is the uncomfortable part. Nine of those eleven sites pass my gate right now. Their safety comes from server leniency, not from their sitemap being right. Change hosting, flip one framework config value, migrate the origin — and those nine flip to 301 together, on a day when nobody touched a page. The two sites I can name today are the ones that already flipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two independent observers, one root cause
&lt;/h2&gt;

&lt;p&gt;Redirects alone would be arguable — maybe Google tolerates them. It does not, and I did not have to guess: Search Console's sitemap report for that same domain went from 2 warnings to 3 in the same week the probe found 11 of 12 URLs redirecting. A server-side probe and a Google-side report, arrived at independently, naming the same file.&lt;/p&gt;

&lt;p&gt;That coincidence is worth more than either observation. When a status probe and a Search Console warning agree on the same URL set, you have a root cause. When only one of them fires, you have a hypothesis.&lt;/p&gt;

&lt;p&gt;The rest of the fleet-wide sitemap errors told the same story from the other direction: five sites were reporting &lt;code&gt;errors&lt;/code&gt;, and all five were the sites whose sitemaps were 100% cross-host URLs. The Google-side error and the XML-side pollution were the same fact recorded by two systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate
&lt;/h2&gt;

&lt;p&gt;What I run now, in order, on every deploy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parse, then probe.&lt;/strong&gt; Validate the XML, then request every URL in it — or a sample of at least 12 per site — with redirects disabled. Format checks and liveness checks are not substitutes for each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probe the URL as listed.&lt;/strong&gt; Do not normalize first. My own first attempt at a control case rewrote the slash before requesting it, which measured a URL that was never in the sitemap and produced a convincing false alarm. The sitemap string is the input to the test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classify redirects by target.&lt;/strong&gt; &lt;code&gt;Location&lt;/code&gt; equal to the source modulo trailing slash is a canonicalisation bug. Anything else is a move. Different fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold per site, never fleet-wide.&lt;/strong&gt; Any site above 5% slash-redirects on ≥8 samples fails the build. A 1.5% fleet average is compatible with one site being at 92%, and the average is what gets reported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry transport errors out of band.&lt;/strong&gt; Curl before believing that a host is down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat 403 and 404 on the alternate spelling as intent.&lt;/strong&gt; They tell you which policy the origin actually implements, which is more reliable than reading the framework's config.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is exotic. It is about thirty lines and a threshold. What made it worth writing down is that for months the check I had was the parse, and the parse was green the entire time — on a fleet where one site had 92% of its URLs answering with a redirect that cost a crawl hop each, and nine more were one config change away from joining it.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>http</category>
      <category>python</category>
    </item>
  </channel>
</rss>
