If you run a Markdown-based blog long enough, the frontmatter starts accumulating rules. "A reviews post must carry an ad disclosure." "FAQ questions in JSON-LD must also appear in the body." Eventually a README check-list isn't enough — you forget. Astro Content Collections plus Zod lets you push most of those rules into build failures. .refine() couples two fields, nested z.object types your structured data, and a violation gets caught at astro build . This post is the code-first version of the setup I use on aulvem.com. The longer version with operational notes is linked at the end. Minimal setup: defineCollection + z.object // src/content.config.ts import { defineCollection , z } from " astro:content " ; import { glob } from " astro/loaders " ; const blog = defineCollection ({ loader : glob ({ pattern : " **/[^_]*.{md,mdx} " , base : " ./src/content/blog " , }), schema : z . object ({ title : z . string (), description : z . string (), pubDate : z . coerce . date (), category : z .…