Astro Collection Schemas — Advanced Validation with Zod
In this tutorial, you will learn about Astro Collection Schemas. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn advanced Astro content collection schemas: Zod validation, image references, nested objects, custom types, and schema reference patterns.
In this lesson, you'll create sophisticated Zod schemas for content collections, validate images and references, define reusable schema fragments, and handle complex frontmatter structures.
What You'll Learn
Zod validation for advanced frontmatter, image references with z.image(), nested object schemas, reusable base schemas, and cross-collection references.
Why It Matters
Real-world content has complex metadata. Advanced schemas catch more errors at build time and provide richer type information for your templates.
Real-World Use
DodaTech's tutorial content uses schemas with nested author objects, image references, and version fields to ensure every page has complete metadata.
flowchart TD
A[Base Schema] --> B[Blog Schema]
A --> C[Doc Schema]
B --> D[Image Ref]
C --> E[Nested Objects]
D --> F[Build-Time Validation]
style A fill:#ff5a03,color:#fff
Reusable Base Schema
Define common fields in a base schema:
import { defineCollection, z, referable } from "astro:content";
const baseSchema = z.object({
title: z.string(),
description: z.string().min(50).max(165),
date: z.date(),
lastmod: z.date().optional(),
draft: z.boolean().default(false),
tags: z.array(z.string()).default([]),
});
const blogCollection = defineCollection({
schema: baseSchema.extend({
author: z.string(),
image: z.image().optional(),
category: z.enum(["tutorial", "news", "review"]),
}),
});
const docsCollection = defineCollection({
schema: baseSchema.extend({
order: z.number(),
category: z.enum(["getting-started", "guides", "api"]),
related: z.array(z.string()).optional(),
}),
});
Image References
Use z.image() for optimized image handling:
const projectCollection = defineCollection({
schema: z.object({
title: z.string(),
screenshot: z.image(),
thumbnail: z.image().optional(),
gallery: z.array(z.image()).default([]),
}),
});
In frontmatter, reference images relative to the content directory:
---
title: My Project
screenshot: "./images/project-screenshot.png"
gallery:
- "./images/screenshot-1.png"
- "./images/screenshot-2.png"
---
Astro validates that the referenced files exist and optimizes them during build.
Nested Object Schemas
Validate complex frontmatter with nested objects:
const courseCollection = defineCollection({
schema: z.object({
title: z.string(),
instructor: z.object({
name: z.string(),
bio: z.string().max(300),
avatar: z.image().optional(),
}),
lessons: z.array(z.object({
title: z.string(),
duration: z.number(),
videoUrl: z.string().url().optional(),
})),
}),
});
Cross-Collection References
Reference entries from other collections:
const authorCollection = defineCollection({
schema: z.object({
name: z.string(),
slug: z.string(),
}),
});
const postCollection = defineCollection({
schema: z.object({
title: z.string(),
author: z.string().refine(async (slug) => {
const authors = await getCollection("authors");
return authors.some(a => a.slug === slug);
}, "Author slug must exist"),
}),
});
Custom Zod Methods
Add custom validation with Zod methods:
const slugSchema = z.string().regex(
/^[a-z0-9]+(?:-[a-z0-9]+)*$/,
"Slug must be kebab-case"
);
const pageCollection = defineCollection({
schema: z.object({
title: z.string(),
slug: slugSchema,
seo: z.object({
title: z.string().max(60).optional(),
description: z.string().max(165).optional(),
}).optional(),
}),
});
Common Mistakes
- Forgetting
.extend()when reusing schemas: UsebaseSchema.extend({...})to inherit fields. Direct assignment loses the base fields. - Using
z.image()without the image service: Image references require the image service integration. Install@astrojs/imageor use the built-in experimental image support. - Making required fields optional by accident: Check that required frontmatter fields exist on every entry. Optional fields should use
.optional()explicitly. - Not validating URL formats: Use
z.string().url()for external links andz.string().startsWith('/')for internal paths to catch invalid URLs at build time. - Over-engineering schemas: Start simple with a few fields and add complexity as your content grows. Overly strict schemas frustrate content authors.
Practice Questions
How do you make one schema inherit fields from another? Answer: Use
.extend().const blogSchema = baseSchema.extend({ author: z.string() })adds the author field to the base fields.What does
z.image()validate? Answer: It validates that the referenced image file exists in the expected location and enables automatic image optimization.How do you create an optional nested object? Answer: Wrap the object schema in
.optional(), e.g.,seo: z.object({...}).optional().Why use custom regex validation on slugs? Answer: To enforce consistent URL-safe naming (kebab-case) and catch invalid characters before deployment.
Challenge
Create a three-collection system: authors, categories, and posts. The post schema should reference author and category slugs, and validate that referenced entries exist.
Mini Project
Build a course platform content structure with collections for instructors, courses, and lessons. Include nested lesson objects with duration validation and image references for instructor avatars.
FAQ
What's Next
Learn about Astro Dynamic Routes to create pages with parameters and catch-all routes.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro