Frontmatter Schemas

Zod Schemas Validate Every Entry at Build Time

Frontmatter Schemas

Each collection has a Zod schema. Astro validates frontmatter against it on every build — typos and missing fields fail fast.

4 min read Level 2/5 #astro#zod#schema
What you'll learn
  • Author a Zod schema
  • Use coercion for dates, numbers, enums
  • Make fields optional, with defaults

The schema on a collection definition is a Zod schema — a description of the shape of each entry’s frontmatter. Astro runs it on every entry at build time, fails the build for mismatches, and types entry.data from it.

A Typical Schema

import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

const blog = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/blog" }),
  schema: z.object({
    title:       z.string(),
    description: z.string().optional(),
    pubDate:     z.coerce.date(),
    updatedAt:   z.coerce.date().optional(),
    tags:        z.array(z.string()).default([]),
    draft:       z.boolean().default(false),
    coverImage:  z.string().optional(),
    author:      z.string(),
  }),
});

export const collections = { blog };

Common Zod Pieces

ZodWhat it expects
z.string()A string
z.string().url()A string that’s a valid URL
z.number()A number
z.boolean()True or false
z.array(z.string())An array of strings
z.coerce.date()A string that gets coerced to a Date
z.enum(["a", "b"])Exactly "a" or "b"
z.object({ ... })A nested object with these fields
.optional()The field may be omitted (or be undefined)
.default(x)Use x when missing
.nullable()Allow null

Why Coerce?

YAML in frontmatter parses some values smartly (numbers, booleans) but dates come through as strings. z.coerce.date() turns the string into a real Date.

---
title: Hi
pubDate: 2026-05-12     # YAML date or string
---

After z.coerce.date(), entry.data.pubDate instanceof Date is true.

Custom Validation

Zod has refinements for cross-field checks:

schema: z.object({
  pubDate:   z.coerce.date(),
  updatedAt: z.coerce.date().optional(),
}).refine(d => !d.updatedAt || d.updatedAt >= d.pubDate, {
  message: "updatedAt cannot be before pubDate",
}),

Schema Function With Helpers

If you need image references, schema can be a function that receives helpers:

import { defineCollection, z } from "astro:content";

const blog = defineCollection({
  loader: glob({ /* ... */ }),
  schema: ({ image }) => z.object({
    title: z.string(),
    coverImage: image(),     // typed image asset, optimizable
  }),
});

image() here is the helper that lets <Image src={data.coverImage} /> work with Astro’s image optimization.

Up Next

Querying collections in pages.

getCollection →