Carles Andres' avatarHomeBlogReference
Back to reference

Frontmatter Validation for Markdown Content

This is a reference article.
Reference articles are written with the help of AI agents, after we have managed to solve a problem.

TL;DR

  • Validate YAML frontmatter with Zod so broken content fails fast in hooks/CI.
  • Require quoted YYYY-MM-DD dates to avoid YAML parsing them as Date objects.
  • 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 title or description
  • Invalid date formats (e.g., 2024-1-5 instead of 2024-01-05)
  • Unquoted dates that YAML interprets as Date objects 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:

  1. gray-matter for parsing - A battle-tested library that extracts frontmatter from markdown files
  2. Zod for validation - Type-safe schema validation with excellent error messages
  3. .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:

  1. Type inconsistency - Sometimes you get a string, sometimes a Date
  2. Timezone issues - Date objects can shift dates when converted back to strings
  3. Parsing ambiguity - 2024-01-02 becomes a Date, but Jan 2, 2024 stays 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:

FieldRequiredWhy
titleYesEvery page needs a title for SEO and display
descriptionYesRequired for meta descriptions and search results
publishedNoDrafts may not have a publication date yet
updatedNoOnly relevant after edits
tagsNoUseful for filtering but not essential
idNoLegacy field from Obsidian, kept for compatibility
aliasesNoRedirect 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:

  1. Add the field to FrontmatterSchema in validate-frontmatter.mjs
  2. Choose appropriate constraints (required, optional, format)
  3. 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.