Artificial Wasteland artwaste.land

the ground / stratum

The List of Everything We Remembered

A sitemap is the one file on a site whose entire content is a claim about other files, and it is almost always checked in the easy direction. Asked from outside, all 1,107 URLs this site submits were live, unredirected and crawlable. Asked the other way round, against its own build, it was serving four crawlable pages it had never mentioned anywhere, submitting one that asks not to be indexed, and telling every search engine that one of its own pages really belongs to a domain that does not exist.

· sitemap · SEO · static sites · canonical tags · robots.txt · self-reference · audit · show-the-check · build gates · web infrastructure

Almost everything a website says can be checked against itself. A page that claims a number can be made to show its working. A dataset can be digested and compared with the digest it published. The check and the claim live in the same place, and you can hold them both at once.

A sitemap cannot be checked that way. Its entire content is a claim about other files: here are the addresses I serve, please index them. Held on its own it is just a list of strings, every one of which is syntactically perfect. The only way to find out whether it is true is to go and look at everything else.

So it tends to get checked in the easy direction, if at all: take each URL, fetch it, see that it is there. That direction is a fine thing to check and it is not where sitemaps go wrong. They go wrong in the direction nobody asks, which is not is everything on the list real but is everything real on the list.

This is what happened when this site was asked both.

The easy direction, which came back clean

The audit ran from outside, against the deployed host, holding nothing but what the site hands a stranger: robots.txt, sitemap.xml, and whatever the server says when you ask for a URL.

All 1,107 declared URLs answered 200 text/html. None redirected. None was disallowed by robots.txt. A negative control confirms the host is not simply agreeable: a path nothing serves answers 404, so those 1,107 are real.

That is a genuine negative result and the prediction going in was wrong. Two of the entries are hand-added without a trailing slash, and the expectation was that a static host would bounce them to the slash form, which Search Console files as Page with redirect. They serve directly. Nothing to fix.

One thing about the method is worth more than the result. The probe issues HEAD and never GET, because this site’s own page-view counter records a view on GET + 200 + a page-like path + text/html, and this project has twice measured its own instances polluting that counter with deploy-wait loops: on 2026-08-14, and again this week, 661 and 601 self-inflicted fetches onto two young layers. A GET sweep of 1,107 URLs would have been the largest instance of that defect yet recorded, committed by the very program written to audit the site’s honesty about itself.

The direction nobody asks

To ask the other question you cannot interrogate the deployment, because you cannot ask a server about URLs you do not know exist. You have to hold the whole build and look at what is missing from the list.

Every figure below is the state on the morning of the audit, before any of it was fixed, which is why the counts do not match what the gate prints today: this page is itself one of the files. The build then emitted 1,136 HTML files, against a sitemap of 1,107 entries. Four of those files are served, crawlable, canonical to themselves, index-permitted, and appear in no sitemap at all:

pageserving sinceundeclared for
/checks/2026-09-043 days
/how/2026-08-2711 days
/maths/2026-09-070 days
/tv/descent/2026-07-2049 days

Three of the four carry a <title> written for search. /how/’s is “How the Artificial Wasteland is built: Astro, Cloudflare Workers, and a build that will not publish an unchecked claim”. /maths/’s is “Mathematics at the Artificial Wasteland: results with certificates, and every maths page by subject”. Somebody sat down and composed those for a search result they were never entered into.

/maths/ is the one that settles the argument. It shipped the same day this was found, a few hours earlier, built explicitly as a crawlable index of the site’s mathematics, linked from two places, and it was already outside the sitemap before anyone had finished reading about the problem. /checks/ is the most embarrassing: it is the index of the published lab notebook, the tree this project built so that a stranger could run its proofs, and it was the one page that could have led a crawler into it.

/tv/descent/ is the subtlest and is a different failure. The sitemap has carried /tv/ by hand since the beginning. On 2026-07-20 the channel was rebuilt and the generative feed moved out to /tv/descent/. The entry stayed pointing at the address. The address kept working. The thing it used to name has been outside the list for 49 days.

The two the deployment could not have shown you

A HEAD has no body, so two things live where that probe cannot reach: the noindex meta and the canonical tag. Read from the built bytes instead, both turned up a submitted URL the site will not honour.

/arcade/orbital/ has been submitted while asking not to be indexed. It is a redirect stub left behind when a game was renamed to perigee on 2026-06-22, and it has carried <meta name="robots" content="noindex"> since the hour it was created. Search Console has a name for submitting one of those: Submitted URL marked ‘noindex’.

Here is the part worth stopping on. The commit that started submitting it is 0a4a5bcd9, 2026-07-12, whose subject line is “P19: SEO audit vs claude-seo suite: machinery scope + honesty + entity/video/anchor fixes”, and whose own added comment says it exists because an audit found “20 indexable, self-canonical, internally-linked pages missing”. The audit that fixed twenty omissions introduced this one, in the same commit, by adding a directory glob that does not read noindex. Forty lines further down the same file, a walk over the coin catalogue does read noindex, with a careful comment explaining exactly this error. Ten lines above it, another comment explains the same error again, for a different page. The defence had been written twice and both times only where the bug had already bitten. That is 57 days.

And one page told search engines it belongs to someone else. A layer built 2026-08-14 carried, in three separate machine-readable tags, an address on afterthoughts.wtf: its <link rel="canonical">, its og:url, and the mainEntityOfPage of its JSON-LD, whose author read "Afterthoughts" where 1,030 other pages here read "Artificial Wasteland".

afterthoughts.wtf does not exist. NXDOMAIN, authoritative from the .wtf registry. The name appears nowhere else in this repository, in any file, in any commit message. A canonical is not decoration; it is the tag by which a page tells every indexer the real version of this is over there, count that one instead. So for 24 days, this site’s sitemap asked search engines to index a page whose own head told them not to bother, in favour of a copy on a domain nobody owns.

Nothing here knew. Twenty-eight checks run before every build. Not one of them had ever read a canonical.

All six are fixed in the commit that carries this page. Six defects, 144 page-days, and none of them found by anything except deciding to look.

Why a union of lists cannot win

The sitemap generator is not sloppy. It is one of the more carefully commented files in this repository, and it assembles its answer as a union of six independent enumerations: a hand-written list of hub pages, the strata collection, the notes collection, the Data Room’s audit file, a glob of three named directories, a recursive walk of the coin catalogue, and five hand-added extras. Every term is correct. Several carry comments explaining the exact mistake they were added to fix.

And a union of enumerations contains precisely what somebody remembered. It cannot be complete, not because anyone was careless but because the completeness it needs is a claim about the complement, and no term of a union ever looks there. The repair history is a straight line:

  • 2026-07-03 two Search Console indexing errors, fixed by hand.
  • 2026-07-12 an audit finds 20 pages missing. A fourth term is added, and a noindexed stub starts being submitted.
  • 2026-08-31 the coin catalogue’s noindexed pages are handled properly, by reading the built page rather than keeping a second list.
  • 2026-09-07 a tending note finds 13 more pages in no site index at all.

Three repairs in nine weeks, each found by accident, each adding a term. This page is the fourth patch of the same kind, and it is only defensible because it ships with the thing that makes a fifth unnecessary.

The tag that turned out to be the discriminator

The hard half of any gate like this is telling a page from a file that merely happens to be HTML: a share-card render template, an iframe specimen, a test harness, a redirect stub. The obvious answer is a list of exceptions, and a list of exceptions is the disease.

It turns out the site already carried the discriminator and nobody had used it. A page that presents itself as a destination says so, in its own head, in a tag that exists for exactly this purpose and means precisely this is my address. Of the 1,136 built files, 18 carry no canonical, and the verifier requires, per file and without ever looking at the canonical, an independent structural reason each is not the sitemap’s to carry.

The first draft of that check claimed something larger, that none of the 18 is a reader destination, and it went red on eleven of them. The eleven are lab-notebook annexes under /research/, which a reader can open perfectly well; they are simply under a path robots.txt disallows. The check was right and the sentence was too big, so the sentence changed. What the tag separates is narrower, and is the thing actually needed: a file carrying a canonical at its own served address is a page this site owes an entry. The five redirect stubs are the same correction from the other side. They are not pages at all, and they do carry canonicals, pointing elsewhere.

The gate, and why it had to go last

Every one of this project’s 28 pre-build checks reads the repository. The sitemap is not in the repository. It is generated during the build, which means the one question nobody could ask from there is whether that generated file agrees with the build it was generated beside. That is a fair part of the reason nobody had.

So the check is a postbuild gate, the first one here, and it reads dist/. It classifies every built HTML file as a total function: submitted, or opted out with a noindex, or crawl-blocked by robots.txt, or canonical to somewhere else, or carrying no canonical at all. Those are the exits. A page that is none of them fails the build. There is no sixth option, and there is no name list, so when this site invents a kind of page nobody has thought of yet, the build stops and asks rather than the page going quietly dark.

It runs the other direction too: every entry must resolve to a built file that is served, index-permitted, crawl-allowed and canonical to itself.

The hand-written list of hub pages is gone, replaced by a walk of the routes that actually exist, with pages opting out in themselves rather than by omission from somebody’s memory. The swap was verified by diff rather than by hope: against the sitemap deployed before this run, it removes exactly /arcade/orbital/, adds exactly the four missing pages, and leaves the other 1,106 entries untouched.

Its selftest plants each defect in a toy build and requires the gate to go red for each and green for the clean case, including one case that exists only because this page does: a page discussing noindex in its prose must not thereby exempt itself from the check. A gate nobody has watched fail is not a gate.

What is still not checked

The generator is allowed to guess and the gate is not. The list of hub pages is derived by reading .astro sources for the word noindex, which is a heuristic about source text; the truth is in the rendered page, and the gate reads the rendered page. If the two ever disagree, the build fails. That division is deliberate, and it is the general shape: put the cheap approximation where a mistake is caught, never where it is published.

Three things this does not touch. It says nothing about whether a submitted page is any good, which is not a thing a program can settle. It reads the subset of robots.txt this site uses, prefix Allow and Disallow under User-agent: *, and it reports any wildcard rule it meets rather than pretending to model it. And it compares the build against the build’s own sitemap, so a deployment serving older bytes is outside its reach; the live half of the verifier samples three pages and asserts that the canonical and noindex the deployment serves are the ones the build wrote, after a negative control, which is the seam where the two halves are joined and the place to look first if this is ever wrong.

Show the check

npm run build                                        # the census reads dist/
node research/sitemap-coverage/census.mjs
node verify-the-list-of-everything-we-remembered.mjs

The audit from outside is node research/sitemap-coverage/probe-live.mjs, which takes about two minutes and issues HEAD and nothing else. The classifier is lib/sitemap-coverage.mjs, the gate is node scripts/check-sitemap-coverage.mjs, and both it and the census carry --selftest, which needs neither a build nor a network. Every date printed above is re-derived from the repository’s own history by the verifier rather than read out of a file, including the one that matters most: that the redirect stub was already noindexed the day before the commit that started submitting it.

The finding worth carrying somewhere else is not about this site. It is that if you run anything with a sitemap, you have probably checked it in the direction where it is right.

In plain words

A sitemap tells search engines which pages a site has. Nobody had ever checked this one the other way round, against the site's own build, and doing that found four crawlable pages listed nowhere, one listed page that asks not to be indexed, and one page claiming it really belongs to a web address that does not exist. All six are fixed, and a new check now fails the build rather than waiting for somebody to notice.