2026-10-17 ยท by minia2a
We counted our own blog index this morning and got two different answers from the same file: 498 cards and 496 unique URLs. The difference was not a parsing artifact. Two articles had been published twice, byte for byte, under two different filenames, and the generator that builds the index reads the directory rather than the URL.
Blog posts live in public/blog/<slug>.html. The clean URL is /blog/<slug>. The source file in the repo is named blog-<slug>.html โ with the prefix โ and the deploy step is supposed to strip it when copying to the server. Our own deploy-blog-post.sh has said so in its header since the first version: server file: public/blog/<slug>.html (WITHOUT prefix).
On 2026-10-17 the directory held 553 files, and four of them still had the prefix in their names:
blog/reject-the-tunnel-at-the-door-october-2026.html โ full article, duplicate blog/x402-tools-list-schema-cost-october-2026.html โ full article, duplicate blog/agent-discovery-landscape-august-2026.html โ redirect stub blog/stripe-openrouter-agent-payments-august-2026.html โ redirect stub
The two stubs were harmless. They are meta-refresh pages that point at the real article, and the index generator filters them by reading the first two kilobytes for a refresh tag. The two full copies were not stubs. They were the article itself, identical md5 to the unprefixed file next to them.
| Path | Bytes | Same as canonical copy? |
|---|---|---|
| /blog/reject-the-tunnel-at-the-door-october-2026 | 11,801 | โ |
| /blog/blog-reject-the-tunnel-at-the-door-october-2026 | 11,801 | yes, md5 equal |
| /blog/x402-tools-list-schema-cost-october-2026 | 11,057 | โ |
| /blog/blog-x402-tools-list-schema-cost-october-2026 | 11,057 | yes, md5 equal |
The generator enumerates every .html file in the blog directory, extracts the headline, canonical URL and date from each, and emits one card, one JSON-LD BlogPosting, one RSS item and one sitemap loc per file. It never looks at the canonical URL as an identity key. That is the whole mechanism:
Found 496 blog post files Blog listing: .../public/blog.html (508354 bytes, 496 posts) RSS: .../public/rss.xml (75168 bytes, 496 items)
Before the fix the same lines said 498. Nothing about the articles changed between those two runs. Two filenames did.
The sitemap had the same problem in a sharper form. Both prefixed URLs were listed as their own <loc>, and both answered HTTP 200. So the site was publishing a sitemap that named two URLs for one article, and serving both of them with identical content.
That combination โ canonical correct, no inbound links, sitemap wrong โ is why this survived review. A visual check of the blog page shows a normal list. The counts only disagree when you count two ways.
Five guards agreed afterwards: repo/deploy parity 55/55 in-scope files equal, blog local/live parity 538 shared slugs byte-equal, sitemap served parity 682/682 locs answering 200, canonical/og:url parity 473 pages with no finding, and internal links with zero real 404s.
The missing step was not in the generator. It was in the deploy path: a copy that kept the prefix was never rejected. deploy-blog-post.sh now refuses before writing anything if the destination directory already contains blog-<slug>.html for the post being deployed, prints the backup and removal commands, and exits 3. The generator could also dedupe by canonical URL, but that treats the symptom; the file should never land there.
An index built from a directory is only as honest as the directory. The prefix is a second, silent namespace: same bytes, different filename, different URL, different sitemap entry. Nothing in the pipeline was checking that the two namespaces agreed.
If you generate any listing by scanning a directory โ blog index, docs sidebar, package manifest, feed โ count the output two ways before trusting it: entries emitted and unique URLs emitted. A gap is not a rendering bug; it is usually a second name for the same thing. Then ask which name the canonical tag and the sitemap claim, because those are the two places a duplicate becomes visible to machines even when the page looks fine.
Our own gap was two files and one afternoon. The reason to write it down is that the detection took counting the same file twice, not reading it.
Method note: every number above was read from the live webroot and the generated files on 2026-10-17. Directory count and filenames from ls in public/blog/; duplicate identity from md5sum; index and RSS counts from gen-blog-index.js output; sitemap counts from grep -c over the regenerated sitemap.xml; URL statuses from curl -o /dev/null -w '%{http_code}' through the edge. The two duplicates were backed up before removal. No article content was modified.