Astro content collections keep your markdown and MDX in a dedicated folder and validate them with a schema. You get TypeScript types for frontmatter and body, and a simple API to list and fetch entries. This avoids typos in frontmatter and keeps content and code in sync.
What are content collections?
Content collections are Astro's built-in system for managing content files:
| Feature | Benefit |
|---|---|
| Schema validation | Catch frontmatter errors at build time |
| TypeScript types | Autocomplete and type checking for content |
| Consistent API | Same query methods for all collections |
| Build-time processing | Content is validated and transformed during build |
| MDX support | Use components in your content |
Instead of manually parsing markdown and hoping frontmatter is correct, you define a schema and let Astro enforce it.
Setting up a content collection
1. Create the content folder
Create a folder under src/content/ for your collection:
src/
content/
blog/
my-first-post.md
getting-started.md
config.ts
2. Define the schema
Create src/content/config.ts to define your collection schemas:
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
draft: z.boolean().default(false),
author: z.string().default('Anonymous'),
tags: z.array(z.string()).default([]),
image: z.string().optional(),
}),
});
export const collections = { blog };
3. Write content
Create markdown or MDX files in the collection folder:
---
title: "Getting Started with Astro"
description: "A beginner's guide to building with Astro"
pubDate: 2026-02-15
tags: ["astro", "tutorial"]
---
Your content here...
If frontmatter is missing required fields or has wrong types, the build fails with a clear error.
Querying content collections
Get all entries
import { getCollection } from 'astro:content';
// Get all blog posts
const allPosts = await getCollection('blog');
// Filter out drafts
const publishedPosts = await getCollection('blog', ({ data }) => {
return data.draft !== true;
});
// Sort by date
const sortedPosts = publishedPosts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
Get a single entry
import { getEntry } from 'astro:content';
const post = await getEntry('blog', 'my-first-post');
if (post) {
console.log(post.data.title); // Typed!
console.log(post.body); // Raw markdown
}
Render content
---
import { getEntry } from 'astro:content';
const post = await getEntry('blog', 'my-first-post');
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<time>{post.data.pubDate.toLocaleDateString()}</time>
<Content />
</article>
Dynamic routes with content collections
Create dynamic pages for each content entry:
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
Schema patterns
Required vs optional fields
schema: z.object({
// Required
title: z.string(),
pubDate: z.coerce.date(),
// Optional
description: z.string().optional(),
// Optional with default
draft: z.boolean().default(false),
tags: z.array(z.string()).default([]),
})
Enums for categories
schema: z.object({
category: z.enum(['tutorial', 'guide', 'news', 'opinion']),
})
Image references
import { defineCollection, z, image } from 'astro:content';
const blog = defineCollection({
schema: ({ image }) => z.object({
title: z.string(),
cover: image(), // Validates image exists in src/
}),
});
Related content references
import { defineCollection, z, reference } from 'astro:content';
const blog = defineCollection({
schema: z.object({
title: z.string(),
relatedPosts: z.array(reference('blog')).optional(),
}),
});
MDX and custom components
For MDX files, the schema applies to frontmatter. Pass custom components to the renderer:
---
import { getEntry } from 'astro:content';
import Callout from '../components/Callout.astro';
import CodeBlock from '../components/CodeBlock.astro';
const post = await getEntry('blog', 'advanced-guide');
const { Content } = await post.render();
---
<Content components={{ Callout, CodeBlock, pre: CodeBlock }} />
Now your MDX content can use <Callout> and custom code block rendering.
Multiple collections
Define multiple collections for different content types:
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
}),
});
const docs = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
section: z.string(),
order: z.number(),
}),
});
const authors = defineCollection({
type: 'data', // JSON/YAML data, not content
schema: z.object({
name: z.string(),
bio: z.string(),
avatar: z.string(),
}),
});
export const collections = { blog, docs, authors };
Common patterns
Filter by tag
const posts = await getCollection('blog');
const reactPosts = posts.filter((post) =>
post.data.tags.includes('react')
);
Group by category
const posts = await getCollection('blog');
const byCategory = posts.reduce((acc, post) => {
const cat = post.data.category;
acc[cat] = acc[cat] || [];
acc[cat].push(post);
return acc;
}, {} as Record<string, typeof posts>);
Pagination
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog');
const sorted = posts.sort((a, b) =>
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
return paginate(sorted, { pageSize: 10 });
}
Benefits over manual parsing
| Manual approach | Content collections |
|---|---|
| Parse frontmatter yourself | Automatic parsing and validation |
| No type safety | Full TypeScript types |
| Runtime errors for bad data | Build-time errors |
| Custom query logic | Consistent API |
| Easy to have inconsistent content | Schema enforces consistency |
Summary
Astro content collections provide:
- Schema validation with Zod for frontmatter
- TypeScript types for autocomplete and error checking
- Build-time validation catches errors early
- Simple query API with
getCollectionandgetEntry - MDX support with custom components
Define your schema once, and Astro ensures all content matches. The Astro content collections docs have the full API and advanced patterns.
