A Type-Safe Content Pipeline with Zod and Content Collections
2 min readcontenttypescriptzod
A blog's frontmatter is a contract: every post must have a title, a description that fits in a search snippet, and a publication date. When that contract is enforced only by convention, it breaks quietly. A typo ships, a description runs long, and nobody notices until a crawler truncates it.
Validate at the boundary
The fix is to parse frontmatter with a schema the moment a file enters the pipeline, and to fail the build when it does not match:
const baseSchema = z.object({
title: z.string().min(1).max(120),
description: z.string().min(50).max(160),
publishedAt: z.string().date(),
draft: z.boolean().default(false),
})
A description shorter than 50 characters is now a build error, not a runtime surprise. The constraint lives next to the code that consumes it.
Derive, do not duplicate
Everything that can be computed at build time should be. Slug, locale, reading time, and headings are all derived from the file and its body — never typed by hand into frontmatter where they can drift:
- the folder name becomes the slug
- the filename (
enorfa) becomes the locale - word count produces a reading time
- heading extraction produces a table of contents
The filesystem is the single source of truth. Never introduce a
publishedorhasTranslationfield — the presence of a file is the fact.
Drafts must vanish
A draft: true post is excluded from routes, listings, the sitemap, and the
alternate-link map. A draft translation must never produce an hreflang
pointing at a page that was never emitted.
The result is a pipeline where an invalid document cannot reach production — the whole class of "the content was silently dropped" bugs disappears.