Skip to content

Emit robots.txt, and gate the base URL it is derived from - #60

Merged
ptr727 merged 7 commits into
developfrom
emit-robots-txt
Aug 8, 2026
Merged

Emit robots.txt, and gate the base URL it is derived from#60
ptr727 merged 7 commits into
developfrom
emit-robots-txt

Conversation

@ptr727

@ptr727 ptr727 commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Replaces #59, which GitHub closed when its base branch was deleted on the #58 merge. Same seven commits, rebased onto develop with #58's two commits dropped, so the diff is only this change: 6 files.

Two things carried over from #59 that matter.

#59 never ran CI, and I reported it as green. test-pull-request.yml triggers on pull_request: branches: [main, develop], and #59 was opened against a feature branch to stack it, so the validation workflow never fired. Its checks=2/2 was two skipped jobs. This PR targets develop, so the gate runs for real.

Six review rounds on #59 found six things, all accepted, none declined. Every fix is in the commits here, and the round-by-round exchange is on #59 for the record:

Round Finding
1–2 Four places claimed this check makes a wrong HUGO_BASEURL visible. It cannot — both sides of the comparison derive from baseURLwithdrawn
3 read_text at strict UTF-8 would raise UnicodeDecodeError and take down the whole parity run, in the one gate written to report a bad robots.txtfixed
4 The summary printed robots : built, advertising … above its own failure — fixed
5 The missing-file message asserted enableRobotsTXT as the cause where three causes are indistinguishable — fixed
6 FAIL robots: 1 problems, which the pre-existing gallery label shared — fixed for both

coverage=PARTIAL at 5 of 6 files held in every one of those rounds, which the runbook says a re-request does not cure. Worth watching whether it clears here now that the base is develop.


The old platform serves a robots.txt today and this site emitted none, so the cutover was not a return to a previous state. It was a move from having crawl directives to having none, on a site that has had them for years, and the sitemap pointer went with them. Hugo emits the file only when enableRobotsTXT is set, which is why /robots.txt answered 404 through the first production deploy.

The Sitemap: line is the load-bearing part, not the rules

Across the interim hostname's first full day, every request for sitemap.xml and feed.xml came from curl — the deploy gate's or the host side's — and none from a crawler, while /robots.txt was requested nine times and 404'd every time, five of those from real agents on a name with no inbound links. A crawler is told where a sitemap is rather than guessing it, and the only thing telling them is the file the cutover deletes.

Every Disallow the old platform serves names a WordPress path this site does not have, so the content that preserves today's behaviour is User-agent: *, no Disallow, and the sitemap:

User-agent: *
Disallow:
Sitemap: https://blog.insanegenius.com/sitemap.xml

The theme's template already derives that URL from the built baseURL, so setting the flag is the whole change and there is no second edit to remember at M7b. Verified across two base URLs rather than reasoned about: a mirror build advertises https://blog.home.insanegenius.net/sitemap.xml and a production build advertises the .com one, from the same bytes.

What check_robots gates, and what it does not

Four failures, each demonstrated failing before the check was trusted:

Case Reported
robots.txt absent <public>/robots.txt does not exist - likely enableRobotsTXT is unset in hugo.yaml, though a partial build or the wrong output directory look identical here
no Sitemap: line robots.txt carries no Sitemap: line - a crawler will not find the sitemap unaided
line naming another origin https://blog.insanegenius.net/sitemap.xml (this build's origin is https://blog.insanegenius.com)
line advertising a sitemap not built https://blog.insanegenius.com/news-sitemap.xml (advertised, but news-sitemap.xml was not built)

Each exits 1, and each prints exactly one summary line, after deciding rather than before.

It does not detect a wrong HUGO_BASEURL. The advertised origin and the canonical origin it is compared against both derive from baseURL, so they agree whenever the build is coherent, including when baseURL was wrong for the target environment. Nothing inside the artifact can see that, so the gap stays on the side that knows which host it is serving: the VPS agent reads the origin out of the deployed sitemap.xml, og:url and feed.xml, which is how the first production deploy was cleared.

What the origin comparison is genuinely for is an origin written rather than derived: a committed static/robots.txt shadowing the template, with the old platform's .com sitemap line pasted into it, is the mistake it catches.

/robots.txt/ reaches the file rather than the home page

/robots.txt/, with a trailing slash, is a URL the old platform served and is in the redirect contract. It resolved through slugs.map as an unresolvable attachment slug, which sends a visitor to the home page — right for a slug nothing claims, wrong for a file that now exists.

Fixed in build-redirects.py rather than in the generated map, because the map is rewritten from the capture and a hand edit does not survive the next regeneration. Confirmed by regenerating first and diffing: the committed maps were byte-identical to a fresh run, so the map diff here is exactly the one line. /osd.xml/ stays pointed at the home page deliberately, being an OpenSearch description this site does not emit.

Verification

This touches deploy/maps/ and hugo.yaml, so the local mirror gate applies rather than CI's green being sufficient:

PASS - the built site honors the URL contract
robots : built, advertising https://blog.home.insanegenius.net/sitemap.xml
==> installing release 20260808-142254
==> served by mirror-production
PASS - 1245 URLs honored

/robots.txt/  301 -> https://blog.home.insanegenius.net/robots.txt
/robots.txt   200 text/plain; charset=utf-8
/osd.xml/     301 -> https://blog.home.insanegenius.net/          (unchanged)

Re-verified after the rebase onto develop: build clean under --panicOnWarning, parity gate PASS.

Production still answers 404 for the file until a deploy carries this, which is recorded in TODO.md rather than assumed.

🤖 Generated with Claude Code

ptr727 and others added 7 commits August 8, 2026 08:16
The old platform serves a robots.txt today and this site emitted none,
so the cutover was not a return to a previous state, it was a move from
having crawl directives to having none on a site that has had them for
years. Hugo emits the file only when enableRobotsTXT is set, which is
why /robots.txt answered 404 through the first production deploy.

The Sitemap: line is the load-bearing part rather than any rule. Across
the interim hostname's first full day no crawler fetched sitemap.xml or
feed.xml once, every request to either coming from curl: a crawler is
told where a sitemap is rather than guessing, and the only thing
telling them is the file the cutover deletes. Every Disallow the old
platform serves names a WordPress path this site does not have, so the
content that preserves today's behavior is User-agent: *, no Disallow,
and the sitemap.

The theme's template already derives that URL from the built baseURL,
so setting the flag is the whole change and there is no second edit to
remember at the cutover. Verified across two base URLs: the mirror
advertises its own sitemap and a production build advertises the site's.

/robots.txt/ with a trailing slash is in the redirect contract and sent
visitors to the home page. It now resolves to the real file, fixed in
build-redirects.py rather than in the generated map, since the map is
rewritten from the capture and a hand edit does not survive. Verified
byte-identical regeneration otherwise: the map diff is that one line.
/osd.xml/ stays pointed at the home page, being an OpenSearch
description this site does not emit.

The check is worth more than the file. Every contract list is path-only
and check-live-urls.sh joins whatever base it is handed, which is what
lets one contract cover four environments and also means a build baked
with the wrong host passes all 1,245 URLs while every canonical tag and
sitemap entry names another site. robots.txt is the one artifact whose
absolute URL a gate can read without being told what to expect, so the
parity check compares it against the origin read from the home page's
canonical link. Four failures gated, all four demonstrated failing
before the check was trusted: file absent, no Sitemap line, a line
naming another origin, and a line advertising a sitemap not built.

Released to the local production mirror and checked live, since this
touches deploy/maps and hugo.yaml: 1245 URLs honored, /robots.txt/ 301s
to /robots.txt, and that file answers 200 as text/plain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two overclaims in comments, both real. The hugo.yaml note said no
crawler has ever fetched this site's sitemap unprompted, where the
evidence is one day of traffic on one hostname, and the docstring said
the site answered 404 for years, where the behaviour is a config flag
rather than a duration. Each now says what was observed and over what
window.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four places said this check makes a wrong HUGO_BASEURL visible. It does
not, and the reason is structural rather than a bug: the advertised
sitemap origin and the canonical origin it is compared against are both
derived from baseURL, so they agree whenever the build is coherent,
including when baseURL held the wrong value for the environment being
deployed to. Nothing inside the artifact can see that, which is exactly
why the VPS side reads the origin out of the deployed sitemap.xml,
og:url and feed.xml and reports the counts either way.

Raised in review, in all four places at once, and correct. Each now
states what the comparison proves, internal consistency, and what it
still catches: an origin written rather than derived, where a committed
static/robots.txt shadowing the template is how that happens, and a
sitemap advertised but never built. A gate described as catching more
than it catches is worse than no gate, because the next person stops
looking for the failure it does not see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…row two claims

Three findings, all correct.

read_text with strict UTF-8 raises out of the entire parity run on an
invalid byte, so the one gate written to report a bad robots.txt would
stack-trace on one instead of failing normally. Confirmed both ways:
strict raises UnicodeDecodeError at position 67 on a file carrying
0xff, and errors=replace reports through the ordinary path. The file
most likely to carry it is a committed static/robots.txt, which is the
case this check is for.

The docstring still said two failures where there are four, and both
the docstring and the README said the Sitemap: line is the only
absolute URL this gate can read. It is not, since site_origin reads the
canonical link and collect_refs reads absolute asset references. The
true and narrower point is that it is the only place an origin is
compared rather than joined.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two findings, both correct.

The advertising line printed before the sitemap-existence check ran, so
the one failing case read as a pass on the line immediately above its
own failure. Now every branch prints exactly once, after deciding:
missing-sitemap reports "1 advertised sitemap(s) not built" and only a
clean run prints what it advertises.

The TODO paragraph describing the cutover loss still said "since this
site emits none" in the present tense, one bullet below the entry
saying the file is now built. It reads as history now, which is what it
is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The message named enableRobotsTXT as the cause. A partial build, a
deleted file, and the wrong output directory reach the identical state,
so asserting one sends a reader to verify a setting that is already
correct. It now names the path it looked for, offers the likely cause as
likely, and says the alternatives are indistinguishable from here, which
is the same shape the orphan messages use for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"FAIL robots: 1 problems" was the report. The noun was a bare string
and a count is always printed beside it, so every label with a plural
noun had the same defect: the pre-existing gallery one prints "1 stray
nodes" on a single stray node, which is the shape that made the new
noun wrong in the same way.

Now a (singular, plural) pair chosen by count. The default stays
"missing" and needs no pair, being count-neutral already. Verified
across both labels at 1 and at many, and the clean run is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 8, 2026 15:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR ensures the site emits a robots.txt (including a sitemap pointer derived from the built baseURL) and adds a deploy gate to verify that the generated robots.txt is present, internally consistent with the build’s origin, and only advertises sitemaps that were actually built. It also updates redirect generation so /robots.txt/ lands on the real file.

Changes:

  • Enable Hugo enableRobotsTXT so /robots.txt is emitted and includes a Sitemap: line derived from the built baseURL.
  • Add a check_robots() gate to check-url-parity.py, and improve failure summary wording to handle singular/plural properly.
  • Update redirect generation and the committed slug map so /robots.txt/ redirects to /robots.txt, plus documentation updates capturing the decision and its limits.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
TODO.md Updates the operational record/decision log for robots.txt, including the gate’s scope and limitations.
hugo.yaml Enables enableRobotsTXT: true to emit robots.txt.
deploy/maps/slugs.map Redirects /robots.txt/ to /robots.txt instead of /.
checks/README.md Documents what the robots gate checks and the hard limit it cannot detect (wrong external base URL).
checks/check-url-parity.py Adds check_robots() and improves failure noun handling (singular/plural), wiring the new gate into the parity run.
checks/build-redirects.py Special-cases /robots.txt/ as a well-known root file redirect during map generation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ptr727
ptr727 merged commit 884c50f into develop Aug 8, 2026
5 checks passed
@ptr727
ptr727 deleted the emit-robots-txt branch August 8, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants