Markdown Post Frontmatter Validation
Coverage
- Required-field enforcement — every post must declare
title,date,slug, andtags; missing fields fail the build at parse time - Date format discipline — ISO 8601 with explicit timezone (
2026-05-06T12:00:00Z); ambiguous formats like2026-05-06or5/6/26are rejected - Slug-to-path consistency — the
slugfield must match the post's directory name; out-of-sync slugs cause silent route conflicts - Controlled-vocabulary tagging — every tag in the post's
tagsarray must appear inlib/content/tag-vocabulary.ts; lowercase, hyphen-separated, no synonyms - Schema evolution — when
lib/content/schema.tschanges, every post's frontmatter is re-validated; existing posts that violate the new schema are flagged before the next build runs - Reserved-field protection — fields like
_id,_internal, or any underscore-prefixed key are reserved for the build pipeline and rejected in author-facing frontmatter
Philosophy
The frontmatter block is the contract every post makes with the site's index, the router, and the renderer. If that contract is loose — if posts can omit fields, use ambiguous dates, or invent ad-hoc tags — the index drifts, routes silently overlap, and the search surface degrades. The cost of catching frontmatter bugs at build time is one re-run; the cost of catching them in production is a broken page or a missing entry in the archive. The rule is: validate at parse time, fail loud, and keep the schema small enough that authors can hold it in their head.
Key Files
| File | Purpose |
|---|---|
content/posts/_template.md |
The canonical template every new post copies — its frontmatter is the worked example of every required field |
lib/content/schema.ts |
The TypeScript schema (Zod or equivalent) that runtime validation calls |
lib/content/parse-frontmatter.ts |
The thin wrapper that reads the YAML block and runs schema.parse() — the failure surface for build-time errors |
Verification
Before merging any change to a post's frontmatter or to the schema:
- Every post has the four required fields:
title,date,slug,tags -
dateis ISO 8601 with timezone (no nakedYYYY-MM-DD, no locale-formatted dates) -
slugmatches the post's directory name exactly — not derived fromtitleat runtime - Every tag in
tagsis present inlib/content/tag-vocabulary.ts(runnpm run check:tagsto confirm) - No underscore-prefixed fields (
_id,_internal, etc.) — those are reserved for the pipeline - Schema changes (
lib/content/schema.ts) are paired with anpm run validate:postspass against the entirecontent/posts/**/*.mdset
Do NOT Use When
| Use instead | When |
|---|---|
| (a generic schema-design skill) | The task is designing a new YAML schema for an unrelated domain |
debugging |
A specific build is failing and you need to reproduce the validation error from logs |
documentation |
The task is writing a runbook or contributor doc about the frontmatter format |
refactor |
The task is restructuring parse-frontmatter.ts without changing the validation contract |