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.
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
| Zod | What 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 →