Data, content, and assets
Validate content data
Give content fields a schema so a missing title, invalid date, or wrong value fails before deployment.
A collection loads entries. A schema says what a valid entry looks like. Add one and Astro checks every file for you at build time.
Astro uses Zod for schemas. Here is one for the notes collection:
import { defineCollection } from 'astro:content'
import { glob } from 'astro/loaders'
import { z } from 'astro/zod'
const notes = defineCollection({
loader: glob({ base: './src/content/notes', pattern: '**/*.md' }),
schema: z.object({
title: z.string(),
description: z.string(),
publishedAt: z.coerce.date(),
topic: z.enum(['astro', 'css', 'javascript']),
draft: z.boolean().default(false)
})
})
export const collections = { notes }
Read it top to bottom. title and description must be strings. publishedAt is coerced into a real Date, so 2026-09-07 in the frontmatter becomes a Date object in data.publishedAt, and you can sort by it. topic must be one of three values. draft is a boolean, and if omitted it’s false.
A valid entry looks like this:
---
title: Scoped styles in Astro
description: How Astro keeps component CSS local.
publishedAt: 2026-09-07
topic: astro
---
No draft field. The default fills it in.
Break it on purpose
Now change topic: astro to topic: cooking and run the build:
npm run build
The build stops with an InvalidContentEntryDataError. It names the collection, the file, and the field: topic received cooking, expected astro, css, or javascript. Fix the file and it builds.
This is the payoff. Without a schema, the bad value flows into your templates. Maybe a filter silently drops the note. Maybe a topic page 404s. You find out from a reader. With a schema, you find out before deploying, at the exact line.
Required, optional, default
Make a field required when every template needs it. title is required because a listing without titles is broken.
Use .optional() only for real variants, like a canonicalUrl most notes don’t have. Don’t use it to avoid cleaning up inconsistent frontmatter. That moves the mess from the content into every template.
Use .default() when a missing value has one obvious meaning. A note with no draft field is published. That’s unambiguous, so a default fits.
The schema also gives you types. In a page, note.data.topic is the union 'astro' | 'css' | 'javascript', not string. Misspell a topic in a comparison and the editor tells you.
A good schema is strict enough to catch mistakes and honest enough to describe the content you actually have. When they disagree, fix the content, not the schema.
Lesson completed