Saving a draft and having someone read it feels like one action. ✋
You save. Roughly half a minute later the words are on the internet, served from a cache in Melbourne to wherever the reader is. One keystroke in, one keystroke out.
It isn’t one action. It’s this:
"build": "astro check && rm -rf public/pagefind dist/pagefind && astro build && pagefind --site dist && cp -r dist/pagefind public/"
I want to take that line apart properly, because it took me an embarrassing amount of time to read calmly, and because one of the things it does is delete stuff — which is never what you want to see first thing in a build script until you know exactly why.
Stage 1 — the gate comes before anything renders 🚧
astro check runs first, and it runs before a single page exists.
If it fails, nothing else executes. No build, no deploy, no CDN purge — the push leaves the site exactly as it was, and the only thing that changed is that the previous version is still up. This is the single most valuable property in the whole chain: the failure mode is “nothing happened” rather than “something bad happened.”
It’s also where content gets validated, not just code. The frontmatter schema is a real schema — pubDatetime has to be a date, tags have to be strings, seoTitle is optional but if you write it, it’s checked as a string. A typo’d field doesn’t quietly become an undefined variable three stages downstream. It stops here, in my editor, while the mistake is still thirty seconds old.
Stage 2 — the rm -rf nobody expects 🧹
rm -rf public/pagefind dist/pagefind
There it is. The build starts by deleting two directories.
The reason is a shape problem. Pagefind writes its search index into dist/pagefind — but public/ is also where this site keeps a copy of that index, and Astro copies public/ wholesale into dist/ on every build. So a previous run’s index sitting in the source tree would be carried into the new build right alongside whatever the fresh index does. Two runs, one directory, no guarantee which bytes win.
Clearing both first means the index that ships is unambiguously the one pagefind is about to produce. It’s not clever code. It’s a line acknowledging that the tool’s output directory and the site’s input directory happen to overlap, and refusing to let that be a surprise.
Stage 3 — astro build does more than render ⚙️
The obvious part: every page becomes static HTML. The less obvious part is everything else that gets manufactured on the way past.
Each post gets its own social card — a real 1200×630 PNG rendered at build time, text and layout and all, not a template with a placeholder. The site-level card comes from a route rather than a file. The sitemap is assembled, the RSS feed is written, and _headers rides along to tell the edge how each class of file may be cached.
Then:
pagefind --site dist && cp -r dist/pagefind public/
Pagefind walks the finished output and builds a client-side search index over it, and the result is copied back into public/ so the next run has it too.
Stage 4 — the part I actually watch 👀
Netlify builds on push. There is no netlify.toml in this repo — it reads the build command straight from package.json, and it doesn’t do anything clever, which I’ve come to appreciate.
What I watch for isn’t the green checkmark. It’s the very last line of the pagefind summary:
Indexed 27 pages
Not because the number itself is interesting — because I already know what it should be. Every post in this site carries data-pagefind-body and nothing else on the site does, so that count has to equal my post count, exactly, every time. If I’ve just published one, it goes up by one. If it doesn’t, a page was silently dropped from the content pipeline while the build still reported success.
It’s the one output I can check without trusting the thing that produced it. A build can succeed and be wrong, and this line is where that stops being invisible.
Stage 5 — two edges, split on purpose 🛰️
Netlify builds and holds the origin. Cloudflare sits in front and serves. That split is deliberate, and I’ve written about the nameserver move itself elsewhere — what matters here is the consequence for this chain: there are now two layers between a reader and my markdown, and _headers decides what each layer is allowed to remember.
HTML → public, max-age=0, must-revalidate → DYNAMIC
/_astro/* → public, max-age=31536000, immutable → HIT
/diagrams/* → public, max-age=86400, must-revalidate
HTML never gets cached, because publishing must be instant — the reader who opens the page a second after I push gets the new words. The fingerprinted assets cache hard, because their filenames change when their content does. The one genuinely mutable thing in the static set — diagrams, which I do edit after publishing — gets a day and then has to re-check.
Which is how I learned this lesson the hard way: a diagram I’d corrected sat stale at the edge for a while, because a build can be correct and a cache still be holding yesterday’s copy.
The gap between passing and being right 🪞
Here’s what I keep having to relearn.
Every stage in this chain answers a specific question. astro check: does this compile? The build: does this render? Pagefind: is this indexed? The cache: is this deliverable? And every one of them can say yes while the thing that actually reaches a human is still wrong — a link preview pointing at a file that never existed, a diagram serving a version I’d already corrected, a page count that was quietly one short.
The chain is thorough. It just isn’t thorough about what I didn’t tell it to care about.
So the last step in publishing isn’t automated and probably can’t be. I open the result somewhere I haven’t already been looking, and I look at it as though I didn’t build it. That’s the only stage that can catch what the other seven were never asked.
It’s a good chain. One keystroke in, seven honest stages, half a minute, and someone in another country is reading it off an edge cache — provided I remember that “the build was green” and “it’s right” have never been the same sentence. 🚦
If seven automated stages can all pass while the thing that ships is still wrong — what’s the last check you refuse to automate, and how did you learn you needed it? 👇
Editor’s note, October 2026: stages 4 and 5 still describe Netlify doing the build. It doesn’t any more — that moved to Cloudflare too, same seven stages, same push, different host. The move is written up here.