Frontmatter Validation for Markdown Content
TL;DR
- Validate YAML frontmatter with Zod so broken content fails fast in hooks/CI.
- Require quoted
YYYY-MM-DDdates to avoid YAML parsing them asDateobjects.- Run validation locally with
bun run --cwd apps/web validate-frontmatter.
This site uses markdown files with YAML frontmatter for all blog posts and reference articles. To prevent malformed content from slipping into production, I implemented automated frontmatter validation using Zod schemas, git hooks, and CI checks.
Why Validate Frontmatter?
YAML frontmatter is powerful but error-prone. Common issues include:
- Missing required fields like
titleordescription - Invalid date formats (e.g.,
2024-1-5instead of2024-01-05) - Unquoted dates that YAML interprets as
Dateobjects instead of strings - Typos in field names that go unnoticed until rendering fails
- Unexpected fields that indicate copy-paste errors
Without validation, these errors surface at build time, runtime, or worse—on the live site.
The Validation Script
The validation logic lives in apps/web/validate-frontmatter.mjs:
Key design decisions:
gray-matterfor parsing - A battle-tested library that extracts frontmatter from markdown files- Zod for validation - Type-safe schema validation with excellent error messages
.strict()mode - Catches unexpected fields (typos, leftover metadata)
Schema Design: The Date Quoting Question
The most interesting schema decision involves date fields. Consider these two YAML approaches:
Why I Require Quoted Dates
YAML 1.1 (used by most parsers including gray-matter) automatically converts unquoted YYYY-MM-DD values to JavaScript Date objects. This causes problems:
- Type inconsistency - Sometimes you get a string, sometimes a
Date - Timezone issues -
Dateobjects can shift dates when converted back to strings - Parsing ambiguity -
2024-01-02becomes aDate, butJan 2, 2024stays a string
The schema enforces strings with a custom error message:
When validation fails, you see:
The Trade-off
Making quotes mandatory adds friction—contributors must remember the quotes. However, the benefits outweigh this cost:
- Predictable types everywhere in the codebase
- No timezone surprises when rendering dates
- Consistent formatting across all content files
Required vs Optional Fields
The schema carefully balances strictness with flexibility:
| Field | Required | Why |
|---|---|---|
title | Yes | Every page needs a title for SEO and display |
description | Yes | Required for meta descriptions and search results |
published | No | Drafts may not have a publication date yet |
updated | No | Only relevant after edits |
tags | No | Useful for filtering but not essential |
id | No | Legacy field from Obsidian, kept for compatibility |
aliases | No | Redirect URLs, rarely needed |
Integration Points
npm Script
The validation runs via a dedicated script in apps/web/package.json:
Run it manually with:
Git Hook (Pre-Push)
Validation runs automatically before every push via Husky. The .husky/pre-push hook contains:
Why pre-push instead of pre-commit?
- Faster commits - No delay when making quick saves
- Batch validation - Check everything before pushing to the remote
- Still catches errors - Problems are caught before they reach the remote
CI Pipeline (GitHub Actions)
The validation also runs in CI as a dedicated step in .github/workflows/ci.yml:
This serves as a safety net for:
- Direct pushes that bypass hooks
- Pull requests from forks
- CI environments where hooks might not run
Developer Experience
When validation fails, the output is clear and actionable:
Each error shows:
- The file path so you can jump directly to it
- The field name that failed
- The reason it failed with human-readable messages
On success:
Adding New Fields
To extend the schema for new frontmatter fields:
- Add the field to
FrontmatterSchemainvalidate-frontmatter.mjs - Choose appropriate constraints (required, optional, format)
- Run validation to ensure existing files comply
Example adding a draft boolean field:
Dependencies
The validation script uses two key packages:
- gray-matter - YAML frontmatter parser
- zod - TypeScript-first schema validation
Both are included as production dependencies since content is processed at build time.