Skip to content

Content Collections

In Next.js you often reach for contentlayer or raw fs + gray-matter to load MDX files with type safety. Astro has content collections built in: put MD/MDX files in src/content/<collection>/, define a schema with zod in src/content.config.ts, and use getCollection('blog') anywhere in your project — fully typed, no third-party library required.

Needs a full Astro project to run. Use npm create astro@latest to scaffold one.

Defining a collection: contentlayer vs Astro

Section titled “Defining a collection: contentlayer vs Astro”
React
// contentlayer.config.ts (Next.js)
import { defineDocumentType, makeSource } from 'contentlayer/source-files';
export const Post = defineDocumentType(() => ({
name: 'Post',
filePathPattern: 'blog/**/*.mdx',
contentType: 'mdx',
fields: {
title: { type: 'string', required: true },
date: { type: 'date', required: true },
tags: { type: 'list', of: { type: 'string' } },
},
}));
export default makeSource({ contentDirPath: 'content', documentTypes: [Post] });
// In a page:
// import { allPosts } from 'contentlayer/generated';
Astro
// src/content.config.ts (Astro)
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()).optional(),
}),
});
export const collections = { blog };
// In a page:
// import { getCollection } from 'astro:content';
// const posts = await getCollection('blog');

The schema lives in src/content.config.ts — Astro validates every file in the collection at build time and surfaces type errors in your editor.

src/content.config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content', // 'content' for MD/MDX, 'data' for JSON/YAML
schema: z.object({
title: z.string(),
description: z.string().optional(),
date: z.coerce.date(),
draft: z.boolean().default(false),
tags: z.array(z.string()).default([]),
}),
});
export const collections = { blog };
src/pages/blog/index.astro
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => !data.draft);
const sorted = posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---
<html lang="en">
<head><meta charset="utf-8" /><title>Blog</title></head>
<body>
<h1>Blog</h1>
<ul>
{sorted.map((post) => (
<li>
<a href={`/blog/${post.slug}/`}>{post.data.title}</a>
<time>{post.data.date.toLocaleDateString()}</time>
</li>
))}
</ul>
</body>
</html>

The second argument to getCollection is an optional filter function — here it removes drafts. post.data is fully typed from the zod schema.

Where do you define the schema for a content collection?
How do you retrieve all entries from a collection named 'blog'?
What is the main advantage of content collections over raw `fs` + `gray-matter`?