TLDR: In a static-site pipeline, validate frontmatter dates at parse time — one regex in
toDate()and bad dates blow up the build before they ever ship.
The setup
I'm rebuilding a client's site in Next.js — posts are markdown files with YAML frontmatter, and the date: field drives sort order, page display, OG tags, everything.
YAML is clever about dates. Write date: 2026-06-04 and the parser hands you a real JS Date object automatically. My toDate() helper coerced that to an ISO string. Clean.
But YAML doesn't always parse to a Date. Write something it can't recognize — date: tomorrow, say, while drafting a post you haven't dated yet — and you get a plain string back.
My original toDate() handled that in one line:
return v instanceof Date ? v.toISOString().slice(0, 10) : typeof v === 'string' ? v : undefined;
Any string. As-is. No validation whatsoever.
The silent break I almost missed
Here's what ISO 8601 dates have going for them: lexicographic sort and chronological sort are the exact same thing. "2026-01-01" < "2026-12-31" just works. That's the whole reason a simple localeCompare on ISO strings gives you a correctly-ordered post feed.
A string like "tomorrow" or "June 4, 2026" poisons that… silently. Posts go out of order. No error. No warning in the build log. You'd only notice when someone asks "why is this old post at the top?"
There was a second bug hiding in formatLongDate too: no upper bound on the day. A typo like 2025-02-32 would happily render "February 32nd." Not a crash. Just a lie on the page.
I could patch formatLongDate to catch that. But that's whack-a-mole — bad data would still be in the content graph, corrupting sorts, bleeding into RSS, anywhere that touches the date field.
The fix that actually worked
Gate it at the ingestion boundary. One regex in toDate():
if (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}($|T)/.test(v)) return v;
return undefined;
Non-ISO string → returns undefined → parsePost() throws:
Missing or non-ISO "date" (need YYYY-MM-DD) in content/posts/my-draft.md
The build fails. HARD. Before Vercel touches a deploy.
I also added d > 31 to formatLongDate as defense-in-depth, so the renderer stays safe no matter what. But the real protection is upstream — at parse time, not display time.
Why this matters to me
This is an SSG (static-site generator, every page pre-built at deploy time) — the build step IS my content type system. If next build finishes cleanly, I want to trust every field that comes out of it.
Scattering defensive coercion across render functions means I'm constantly second-guessing whether what I'm rendering is actually clean. One pointed error at parse time means I fix it once, in the right place, before anything ships.
Validate at the ingestion boundary. Let the build fail loudly. Ship only clean data.