A static site has no runtime. When something is in a bad state there's no code around to throw — slightly wrong HTML just gets deployed, and the browser never complains.
So bad states persist. Three rules broke repeatedly on this site.
All three were documented. All three broke. All three now fail the build — and none has broken since.
1. What Actually Happens When They Break
The important property they share: none of them is visible right away. That's precisely why they belong in the build.
A post in only one locale
The layout emits two hreflang links on every post — the Korean page points at the English URL and vice versa. Ship one locale and the live page advertises a URL that doesn't exist.
In a browser everything looks perfect; <link> tags are invisible. Only search engines see the inconsistency.
Dates that disagree
This one is nastier. Scheduled publishing decides per file from its own date, so Korean on August 30 and English on September 1 produces exactly the state above for two days — with both files present in the repo the whole time.
Nothing looks wrong at commit time, it heals itself two days later, and nobody notices in between. It is not the kind of error a human catches.
Links pointing forward
Link from the August 30 post to the September 4 post and it 404s for five days. When you write the link the target file is sitting right there, so there's apparently nothing to check.
Code review can't catch these. A reviewer looks at files, while the defect lives in when those files enter the output.
2. Check Everything Once, Before the Build
Eleventy fires an eleventy.before event once, ahead of template processing. We read the source files there directly.
Running once per build rather than per post matters. Symmetry and link direction are properties of the whole set — no single post carries enough information to judge them.
Comparing two file listings is the entire check. Ten lines outperform a page of documentation.
3. Read Each File Exactly Once
The remaining checks need file contents. Rather than let each one read independently, we read everything up front and reuse it.
Note that date is scraped with a regex and kept as a string. No YAML parser, no conversion to a Date.
Because dates are YYYY-MM-DD, lexicographic comparison is date comparison. No time zones, no parse failures, no dependency. Different string means different date; greater string means later date.
A complicated guard grows bugs of its own — and nothing is checking the guard.
4. Date Agreement and Link Direction
Both remaining checks run off that one posts map.
The link check covers two failures at once: does the target exist (typos, deleted posts) and does it publish no later than the linking post. The second exists only because of scheduled publishing.
Scraping links with a regex looks crude, and here it's entirely sufficient — the scope is same-locale post links in a fixed format. There's no argument for pulling in an HTML parser.
5. Make the Error Message the Fix Instruction
Building these guards, the messages took longer to get right than the code.
The good one delivers what, where, and why at once. That middle sentence — "publish times will diverge" — carries real weight: someone who has no idea why the dates must match understands after reading it.
And violations are collected and reported together. Stopping at the first turns fixing into a fix-rerun loop. It matters especially when an AI agent is doing the work: one failed build tells it everything left to do.
6. Deciding What Deserves a Guard
Not every rule belongs in the build. We picked these three on three tests.
- Does it break silently? Errors that still look correct in a browser after deploy are the top priority.
- Is it mechanically decidable? These reduce to comparing file listings and date strings.
- Has it actually broken? Guard real incidents, not hypothetical risks.
What we left out is equally clear. "The English version shouldn't read like machine translation" is a real rule with no possible automated check. That kind stays in the document, reviewed by a person.
For borderline cases, the tie-breaker is how long it takes to notice. Errors you see immediately don't need a guard. Errors you find weeks later in a search console do.
7. Write Down the Gaps That Remain
Guards create their own trap: "the build passed, so it's fine."
So the rules file lists what the build does not catch, explicitly.
The second item catches us regularly. sitemap.xml fills in posts automatically from a collection, but non-post pages come from a hand-maintained list. Move a page and that list has to move with it — and the build has no idea. It's the next candidate to become a guard.
8. In Summary
Sixty lines all told, and those sixty lines held far better than a page of documentation.
- Start with the rules that break silently. The loud ones already get caught.
- Check whole-set properties once, before the build.
- Keep validation code simple. Dates as strings mean no parser and no time zones.
- Make messages actionable, and batch the violations.
- State what the guards don't cover so a green build never means "everything was checked."
If you're bolding the same sentence in a rules file for the third time, that's a signal the rule shouldn't live in a document. Enforce what can be enforced, and write down only what can't.