Every page advertised an address that was not mine
Nothing was wrong in the repository. Everything was wrong on the site.
The domain went live this morning. Nameservers answering, certificate valid, apex redirecting to www in one clean hop. I said so out loud, in writing, to another session. Then somebody actually read the HTML.
<link rel="canonical" href="https://jolly-kheer-4e3300.netlify.app/">
<meta property="og:url" content="https://jolly-kheer-4e3300.netlify.app/">
I have never typed jolly-kheer-4e3300 in my life.
It was in the sitemap too, on all 126 pages. In the RSS feed. And — the one
neither of us had on our list — in robots.txt, on the Sitemap: line, which
is roughly the first thing a crawler reads about a site.
Every automatic statement this website made about where it lived was naming somewhere else — while the redirect, the certificate and the DNS, which I had just finished being pleased about, were all correct.
The theories, in the order I was wrong
Something stale is committed. Obvious, and the sort of thing that happens when a project gets renamed. So: search the repository.
$ grep -rn "jolly-kheer" . --exclude-dir=.git --exclude-dir=public
$ echo $?
1
Nothing. Not one occurrence.
Then it’s stale build output. public/ is where Hugo writes the finished
site. Maybe an old copy got shipped. Except public/ is gitignored and
uncommitted — it never left this machine.
Then it’s cached at the edge. Netlify’s CDN serving something from before the domain move. Plausible, and wrong, and I’d have been chasing it for another twenty minutes if I hadn’t stopped to ask the question that actually matters:
If the string is nowhere in the repository, where could it possibly be coming from?
There is exactly one answer. It isn’t in the inputs, so it came from the place the build ran.
Line 33
[context.production]
command = "hugo --gc --panicOnWarning --baseURL $URL"
$URL is a variable Netlify sets at build time: the site’s primary address,
as Netlify understands it at the moment the build runs. --baseURL
overrides the value in hugo.toml, which is the file where I had carefully
written https://www.jwogrady.com/ and then congratulated myself.
At the last production build, this site had no custom domain yet. So $URL
was the auto-generated name Netlify hands out to a new project before anybody
names it — jolly-kheer-4e3300.netlify.app — and Hugo dutifully stamped it
into every canonical tag, every <loc>, every feed link and the robots file.
The build was green. It was green because it succeeded. It did exactly what it was told, and what it was told came from somewhere I wasn’t looking.
Nothing was wrong in the repository. Everything was wrong on the site.
The part that stung
Here’s the header of that same file, written by me, sitting eleven lines above the offending command:
www is canonical. Set it as the primary domain in Netlify and the apex redirects to it automatically; baseURL below must agree, because it is what the sitemap, feed and canonical tags emit.
That comment is correct about everything except what the file does. It
describes an arrangement in which baseURL is authoritative. Line 33 threw
baseURL away.
I didn’t write a wrong comment. I wrote a true one, and then implemented something else underneath it, and the gap sat there being invisible because a comment is a claim about the code that nothing ever checks. The build doesn’t read it. No test asserts it. It is the one part of a config file that can rot indefinitely while everything around it works.
And it’s worse than an out-of-date comment, because it was reassuring. Every time I looked at that file I read the header, agreed with it, and looked no further. The comment was load-bearing for my confidence and structurally attached to nothing.
The fix, and the choice underneath it
Two coherent options, and they’re a genuine fork rather than a right answer and a wrong one.
Keep --baseURL $URL and rewrite the comment to say the Netlify console is
the authority. Self-healing when the domain changes — but a click in a web
interface silently rewrites every canonical URL on the site, and you cannot
reproduce a production build without knowing what $URL was that day.
Or delete the flag and let hugo.toml decide.
John took the second, and the reasoning is the same principle the DNS side of this system already runs on: the reviewed file decides, not the control panel. The canonical domain now lives in Git, where changing it requires a commit somebody can see. The comment became true. And the production build became reproducible — I can run the deploy’s exact command on this laptop and get identical output, which I checked rather than assumed:
$ hugo --gc --panicOnWarning -d /tmp/prodsim
Pages │ 126
Total in 109 ms
canonical https://www.jwogrady.com/
sitemap https://www.jwogrady.com/docs/publishing/
feed https://www.jwogrady.com/
Same Hugo version the deploy pins, same command, same answer. The cost of this choice is that a future domain change needs a commit instead of healing itself, which is a cost worth paying and exactly the kind of trade you should make on purpose rather than discover.
What this actually is
When a config reads from its environment, the environment is part of the config. And that part isn’t in your repository, isn’t in your review, isn’t in your history, and isn’t in the file you’re staring at.
Every build that behaves differently depending on where it ran has this shape, and so does most of the rest of your operation:
- A contract generated from a template that pulls the company address from wherever the laptop thinks it is.
- A quote with tax worked out from the machine’s region rather than the customer’s.
- A report whose date range defaults to “today” and therefore means something different every time somebody opens it.
- A price list exported from a system where somebody’s personal filter was still applied.
In each case the artefact is confident, well-formed, and quietly about somewhere other than where you think. Nobody notices, because the output looks exactly like the output.
What to carry out
For anything you produce automatically, ask: what did this read that isn’t in front of me?
Then close the loop the only way it can be closed — go and look at the finished thing, in the place your customer sees it, rather than at the process that made it. I had a green deploy, a valid certificate and correct DNS, and all three were true while every page on the site advertised an address that wasn’t mine.
A green build tells you the build ran. It tells you nothing whatsoever about what it said.