Content Collections represent one of Astro's most powerful features for managing content-driven websites. They provide a structured, type-safe way to organize and validate your content, eliminating common errors and improving developer experience significantly.
What are Content Collections?
Content Collections allow you to define schemas for your content, enabling Astro to validate your markdown, MDX, or data files at build time. This approach provides:
- TypeScript Support: Full autocompletion and type checking for your content
- Schema Validation: Catch content errors before deployment
- Performance: Optimized data loading and querying
- Developer Experience: Excellent IDE support with IntelliSense
Setting Up Content Collections
Enable Collections
Create a ts.config.json file to enable TypeScript support:
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}Astro automatically detects content collections in the src/content/ directory.
Creating Your First Collection
Define the Schema
Create src/content/config.ts to define your collection:
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.coerce.date(),
author: z.string(),
image: z.string().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = {
blog: blogCollection,
};Create Content Files
Add markdown files to src/content/blog/:
---
title: "My First Blog Post"
description: "This is my first post using Astro Content Collections"
publishDate: 2024-03-20
author: "Jane Developer"
tags: ["astro", "webdev"]
draft: false
---
# My First Blog Post
Welcome to my blog post powered by Astro Content Collections!Querying Collections
Get All Entries
---
import { getCollection } from 'astro:content';
const allPosts = await getCollection('blog');
// Filter out drafts
const publishedPosts = allPosts.filter(post => !post.data.draft);
---
<ol>
{publishedPosts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
<time>{post.data.publishDate.toISOString()}</time>
</li>
))}
</ol>Get Single Entry
---
import { getEntry } from 'astro:content';
import { render } from 'astro:content';
const post = await getEntry('blog', 'my-first-post');
const { Content } = await render();
---
<h1>{post.data.title}</h1>
<time>{post.data.publishDate.toISOString()}</time>
<Content />Advanced Schema Validation
Nested Objects
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
author: z.object({
name: z.string(),
bio: z.string().optional(),
twitter: z.string().optional(),
}),
}),
});Enums and Choices
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
category: z.enum(['tutorial', 'article', 'news']),
difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
}),
});Custom Validation
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string().min(10).max(100),
slug: z.string().regex(/^[a-z0-9-]+$/),
email: z.string().email(),
}),
});Working with Images
Image References
import { defineCollection, z, reference } from 'astro:content';
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
heroImage: z.object({
src: z.string(),
alt: z.string(),
}),
// Reference to another collection
author: reference('authors'),
}),
});Multiple Collections
Blog and Authors
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
author: reference('authors'),
}),
});
const authors = defineCollection({
type: 'data',
schema: z.object({
name: z.string(),
bio: z.string(),
}),
});
export const collections = { blog, authors };Querying with References
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
const postsWithAuthors = await Promise.all(
posts.map(async (post) => {
const author = await post.data.author();
return { ...post, author };
})
);
---Sorting and Filtering
Sort by Date
const posts = await getCollection('blog');
const sortedPosts = posts.sort((a, b) =>
b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
);Filter by Tags
const posts = await getCollection('blog');
const taggedPosts = posts.filter(post =>
post.data.tags.includes('typescript')
);Get Related Posts
async function getRelatedPosts(currentPost: CollectionEntry<'blog'>) {
const posts = await getCollection('blog');
return posts
.filter(post => post.id !== currentPost.id)
.filter(post =>
post.data.tags.some(tag =>
currentPost.data.tags.includes(tag)
)
)
.slice(0, 3);
}Data Collections
JSON and YAML Files
Create src/content/products/product-1.json:
{
"name": "Product 1",
"price": 99.99,
"description": "An amazing product"
}Define the schema:
const products = defineCollection({
type: 'data',
schema: z.object({
name: z.string(),
price: z.number(),
description: z.string(),
}),
});Pagination
Paginated Blog Listings
---
import { getCollection } from 'astro:content';
import Pagination from '../../components/Pagination.astro;
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog');
const publishedPosts = posts.filter(post => !post.data.draft);
return paginate(publishedPosts, { itemsPerPage: 10 });
}
const { page } = Astro.props;
---
<ol>
{page.data.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</li>
))}
</ol>
<Pagination page={page} />RSS Feeds
Generate RSS
// rss.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: 'My Blog',
description: 'My awesome blog',
site: context.site,
items: posts.map(post => ({
title: post.data.title,
pubDate: post.data.publishDate,
description: post.data.description,
link: `/blog/${post.slug}/`,
})),
});
}Performance Optimization
Reduce Bundle Size
Content Collections are tree-shaken, so unused data doesn't increase your bundle size.
Build-Time Optimization
All data fetching happens at build time, so your runtime performance is optimal.
Best Practices
1. Use Strict Schemas
Define comprehensive schemas to catch errors early:
schema: z.object({
required: z.string(),
optional: z.string().optional(),
withDefault: z.number().default(0),
})2. Leverage TypeScript
Use TypeScript throughout for better autocomplete:
const posts = await getCollection('blog');
posts.forEach(post => {
// post.data is fully typed
console.log(post.data.title);
});3. Organize Content Hierarchically
Use nested directories for organization:
src/content/blog/
├── 2024/
│ ├── 03/
│ │ ├── post-1.md
│ │ └── post-2.md
└── 2024/
└── 04/
└── post-3.md4. Use Frontmatter Defaults
Set default values in your schema:
schema: z.object({
draft: z.boolean().default(false),
tags: z.array(z.string()).default([]),
})5. Validate External Data
Use collections to validate external data:
import { defineCollection } from 'astro:content';
import externalData from '../data/external.json';
const validatedData = blogCollection.schema.parse(externalData);Real-World Example
Complete Blog Setup
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string().min(5).max(100),
description: z.string().min(50).max(200),
publishDate: z.coerce.date(),
author: z.object({
name: z.string(),
avatar: z.string().url(),
bio: z.string(),
}),
image: z.object({
src: z.string(),
alt: z.string(),
}),
tags: z.array(z.string()).default([]),
category: z.enum(['dev', 'design', 'career']),
draft: z.boolean().default(false),
featured: z.boolean().default(false),
}),
});
export const collections = { blog };Conclusion
Astro Content Collections provide a robust, type-safe foundation for managing content at scale. By defining schemas and leveraging TypeScript, you can build content-driven websites that are both performant and maintainable.
The combination of build-time validation, excellent developer experience, and runtime performance makes Content Collections an essential tool for any serious Astro project.
Next up: Learn how to extend Astro's capabilities with powerful integrations.