Understanding Content Collections in Astro
Learn how Astro Content Collections provide strict schema validation, type safety, and efficient querying for content-heavy sites.
Listen to Article
PlayingClick play to listen to audio narration

Table of Contents
Content Collections are the recommended way to manage Markdown and MDX content in Astro. They provide strict Zod schema validation and type-safe content queries.
Defining a Collection Schema
In src/content.config.ts, define your content structure using Zod schemas:
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
}),
});
export const collections = { blog };
Querying Content in Pages
Querying posts is straightforward and fully typed:
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
---
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.id}/`}>{post.data.title}</a>
</li>
))}
</ul>
Benefits of Content Collections
- Type Safety: Automatic TypeScript inference for frontmatter fields.
- Build-Time Errors: Missing or invalid frontmatter triggers explicit build errors.
- Performance: Optimized data loading for large content repositories.
What Are Content Collections?
A content collection is a directory inside src/content/ that Astro manages automatically. Astro generates TypeScript types for your frontmatter, validates entries against a schema at build time, and provides a type-safe API to query your content.
Without content collections, you would manually parse Markdown frontmatter and hope it matches what your templates expect. With collections, if a required field is missing or has the wrong type, the build fails with a clear error.
Defining a Collection
Create src/content.config.ts (or .js):
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({
base: './src/content/blog',
pattern: '**/*.{md,mdx}',
}),
schema: z.object({
title: z.string(),
description: z.string().optional(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
Querying Collections
---
import { getCollection, getEntry } from 'astro:content';
// Get all published posts
const posts = await getCollection('blog', ({ data }) => !data.draft);
// Get a single post by slug
const post = await getEntry('blog', 'getting-started-with-astro-5');
---
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
</li>
))}
</ul>
Rendering Collection Entries
To render the body of a collection entry (convert Markdown/MDX to HTML), use the render() function:
---
import { render } from 'astro:content';
const post = Astro.props.post;
const { Content, headings } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
headings is an array of all headings in the document β useful for generating a Table of Contents.
Using Glob Patterns
The glob loader accepts an array of patterns, which supports exclusion:
loader: glob({
base: './src/content/blog',
pattern: [
'**/*.{md,mdx}',
'!draft-*.mdx', // Exclude drafts
'!**/internal/*.md', // Exclude internal notes
],
}),
Dynamic Routes with Collections
Generate one page per collection entry using Astroβs static path generation:
---
// src/pages/blog/[...slug].astro
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
Schema Validation at Build Time
If a required frontmatter field is missing or has the wrong type, astro build fails immediately:
error [content] Invalid frontmatter in "blog/my-post.mdx"
- pubDate: Required
This eliminates an entire class of runtime errors from type mismatches between content and templates.
Custom Loaders (Astro 5)
Content Layer in Astro 5 supports custom loaders β fetch from any source:
import { defineCollection } from 'astro:content';
const news = defineCollection({
loader: async () => {
const res = await fetch('https://api.example.com/articles');
const articles = await res.json();
return articles.map(a => ({
id: a.slug,
data: { title: a.title, pubDate: new Date(a.createdAt) },
body: a.content,
}));
},
schema: z.object({ title: z.string(), pubDate: z.date() }),
});Written by Editorial Team
Tech contributor covering software architecture, AI research, cloud infrastructure, and systems engineering practices.