Content Collections are Astro’s solution for managing content like blog posts, documentation, or any structured data. They provide type-safety, validation, and automatic TypeScript types for your content.
What are Content Collections?
Content Collections organize your content into typed, validated groups. Instead of manually loading markdown files, you define collections with schemas and let Astro handle the rest.
Key Benefits : Type-safe frontmatter, automatic TypeScript types, content validation, and optimized image handling.
Setup
Create a content.config.ts file in your src/ directory:
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 (),
author: z . string (),
image: z . string (). optional (),
}),
});
export const collections = { blog };
Organize your content:
src/
├── content/
│ └── blog/
│ ├── post-1.md
│ ├── post-2.md
│ └── post-3.mdx
└── content.config.ts
Defining Collections
Use defineCollection() to create a collection:
import { defineCollection } from 'astro:content' ;
import { glob } from 'astro/loaders' ;
import { z } from 'astro/zod' ;
const blog = defineCollection ({
// Where to load content from
loader: glob ({
base: './src/content/blog' ,
pattern: '**/*.{md,mdx}'
}),
// Validate frontmatter
schema: z . object ({
title: z . string (),
pubDate: z . coerce . date (),
tags: z . array ( z . string ()). default ([]),
}),
});
export const collections = { blog };
Basic Collection
With Images
Multiple Collections
const blog = defineCollection ({
loader: glob ({ pattern: '*.md' }),
schema: z . object ({
title: z . string (),
date: z . date (),
}),
});
const blog = defineCollection ({
loader: glob ({ pattern: '*.md' }),
schema : ({ image }) => z . object ({
title: z . string (),
cover: image (),
}),
});
const blog = defineCollection ({ /* ... */ });
const docs = defineCollection ({ /* ... */ });
const authors = defineCollection ({ /* ... */ });
export const collections = { blog , docs , authors };
Schema Validation
Schemas use Zod for runtime validation:
import { z } from 'astro/zod' ;
const blog = defineCollection ({
loader: glob ({ pattern: '**/*.md' }),
schema: z . object ({
// Required string
title: z . string (),
// Optional string with default
description: z . string (). default ( 'No description' ),
// Transform string to Date
pubDate: z . coerce . date (),
// Optional date
updatedDate: z . coerce . date (). optional (),
// Array with default
tags: z . array ( z . string ()). default ([]),
// Enum
status: z . enum ([ 'draft' , 'published' , 'archived' ]),
// Boolean with default
featured: z . boolean (). default ( false ),
// Nested object
author: z . object ({
name: z . string (),
email: z . string (). email (),
}),
}),
});
Image Schemas
Use the image() helper for optimized images:
const blog = defineCollection ({
loader: glob ({ pattern: '**/*.md' }),
schema : ({ image }) => z . object ({
title: z . string (),
heroImage: image (),
thumbnail: z . optional ( image ()),
}),
});
In your markdown:
---
title : My Post
heroImage : ./hero.jpg
thumbnail : ./thumb.png
---
Post content here.
Querying Collections
Get All Entries
Use getCollection() to fetch all entries:
src/pages/blog/index.astro
---
import { getCollection } from 'astro:content' ;
const posts = await getCollection ( 'blog' );
---
< ul >
{ posts . map (( post ) => (
< li >
< a href = { `/blog/ ${ post . id } ` } >
{ post . data . title }
</ a >
< time > { post . data . pubDate . toLocaleDateString () } </ time >
</ li >
)) }
</ ul >
Filter Entries
Filter with a callback function:
---
import { getCollection } from 'astro:content' ;
// Get only published posts
const posts = await getCollection ( 'blog' , ( entry ) => {
return entry . data . status === 'published' ;
});
// Get posts by tag
const reactPosts = await getCollection ( 'blog' , ( entry ) => {
return entry . data . tags . includes ( 'react' );
});
// Sort by date
const sortedPosts = posts . sort (( a , b ) =>
b . data . pubDate . valueOf () - a . data . pubDate . valueOf ()
);
---
Get Single Entry
Use getEntry() to fetch a specific entry:
src/pages/blog/[id].astro
---
import { getEntry } from 'astro:content' ;
const post = await getEntry ( 'blog' , Astro . params . id );
if ( ! post ) {
return Astro . redirect ( '/404' );
}
const { Content } = await post . render ();
---
< article >
< h1 > { post . data . title } </ h1 >
< time > { post . data . pubDate . toLocaleDateString () } </ time >
< Content />
</ article >
Rendering Content
Call render() on an entry to get the content component:
---
import { getEntry } from 'astro:content' ;
const post = await getEntry ( 'blog' , 'my-post' );
const { Content , headings , remarkPluginFrontmatter } = await post . render ();
---
< article >
< h1 > { post . data . title } </ h1 >
<!-- Table of contents -->
< nav >
{ headings . map (( heading ) => (
< a href = { `# ${ heading . slug } ` } >
{ heading . text }
</ a >
)) }
</ nav >
<!-- Rendered content -->
< Content />
</ article >
The Content component is the rendered markdown/MDX. headings contains the document outline.
Dynamic Routes
Generate routes from collections:
src/pages/blog/[...slug].astro
---
import { getCollection } from 'astro:content' ;
import BlogLayout from '../../layouts/BlogLayout.astro' ;
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 post . render ();
---
< BlogLayout
title = { post . data . title }
description = { post . data . description }
pubDate = { post . data . pubDate }
>
< Content />
</ BlogLayout >
Loaders
Loaders determine where content comes from. The glob() loader reads files from disk:
import { glob } from 'astro/loaders' ;
const blog = defineCollection ({
loader: glob ({
base: './src/content/blog' ,
pattern: '**/*.{md,mdx}' ,
}),
schema: z . object ({ /* ... */ }),
});
You can also create custom loaders to fetch content from APIs, databases, or CMSs.
References Between Collections
Create relationships between collections:
import { defineCollection , reference } from 'astro:content' ;
import { glob } from 'astro/loaders' ;
import { z } from 'astro/zod' ;
const authors = defineCollection ({
loader: glob ({ pattern: '*.json' }),
schema: z . object ({
name: z . string (),
bio: z . string (),
}),
});
const blog = defineCollection ({
loader: glob ({ pattern: '**/*.md' }),
schema: z . object ({
title: z . string (),
author: reference ( 'authors' ),
}),
});
export const collections = { blog , authors };
Query referenced entries:
---
import { getEntry } from 'astro:content' ;
const post = await getEntry ( 'blog' , 'my-post' );
const author = await getEntry ( 'authors' , post . data . author );
---
< article >
< h1 > { post . data . title } </ h1 >
< p > By { author . data . name } </ p >
</ article >
Type Safety
Astro generates TypeScript types automatically:
// Generated types are available globally
import type { CollectionEntry } from 'astro:content' ;
type BlogPost = CollectionEntry < 'blog' >;
function formatPost ( post : BlogPost ) {
// post.data is fully typed!
return {
title: post . data . title ,
date: post . data . pubDate . toISOString (),
};
}
Implementation Details
From the source code at src/content/runtime.ts, collections use:
Data Store : Content entries are stored in a global data store
Schema Validation : Zod schemas validate entries at build time
Type Generation : TypeScript types are auto-generated from schemas
// Simplified from src/content/runtime.ts
export function createGetCollection ({ liveCollections }) {
return async function getCollection (
collection : string ,
filter ?: ( entry : any ) => unknown
) {
const store = await globalDataStore . get ();
const result = [];
for ( const rawEntry of store . values ( collection )) {
const data = updateImageReferencesInData (
rawEntry . data ,
rawEntry . filePath ,
imageAssetMap
);
let entry = { ... rawEntry , data , collection };
if ( filter && ! filter ( entry )) {
continue ;
}
result . push ( entry );
}
return result ;
};
}
Practical Examples
Blog with Categories
const blog = defineCollection ({
loader: glob ({ pattern: '**/*.md' }),
schema: z . object ({
title: z . string (),
description: z . string (),
category: z . enum ([ 'tech' , 'design' , 'business' ]),
pubDate: z . coerce . date (),
tags: z . array ( z . string ()). default ([]),
}),
});
src/pages/blog/category/[category].astro
---
import { getCollection } from 'astro:content' ;
export async function getStaticPaths () {
const categories = [ 'tech' , 'design' , 'business' ];
return categories . map (( category ) => ({
params: { category },
}));
}
const { category } = Astro . params ;
const posts = await getCollection ( 'blog' , ( entry ) => {
return entry . data . category === category ;
});
---
< h1 > { category } Posts </ h1 >
< ul >
{ posts . map (( post ) => (
< li >< a href = { `/blog/ ${ post . id } ` } > { post . data . title } </ a ></ li >
)) }
</ ul >
const docs = defineCollection ({
loader: glob ({ pattern: '**/*.mdx' }),
schema: z . object ({
title: z . string (),
description: z . string (),
order: z . number (). default ( 0 ),
sidebar: z . object ({
label: z . string (),
order: z . number (),
}),
}),
});
src/components/DocsSidebar.astro
---
import { getCollection } from 'astro:content' ;
const docs = await getCollection ( 'docs' );
const sorted = docs . sort (( a , b ) =>
a . data . sidebar . order - b . data . sidebar . order
);
---
< nav >
< ul >
{ sorted . map (( doc ) => (
< li >
< a href = { `/docs/ ${ doc . id } ` } >
{ doc . data . sidebar . label }
</ a >
</ li >
)) }
</ ul >
</ nav >
Best Practices
Define clear schemas
Use descriptive field names and provide defaults where appropriate.
Validate early
Let Zod catch errors at build time, not runtime.
Use TypeScript
Take advantage of auto-generated types for type safety.
Organize by collection
Group similar content together (blog, docs, authors).
Filter in queries
Filter collections at query time for flexibility.
Use enums Define allowed values with z.enum() for better validation.
Reference related content Use reference() to create relationships between collections.
Optimize images Use the image() helper for automatic image optimization.
Default values Provide sensible defaults to make frontmatter easier to write.
Learn More
Routing Generate routes from collections
Layouts Create layouts for your content