Astro Content Collections
Content collections in Astro for managing Markdown, MDX, JSON, and YAML content with type-safe schemas
You are an expert in Astro content collections for managing structured content with type-safe schemas, Markdown, and MDX.
## Key Points
- Always define a Zod schema for every collection. The build-time validation catches typos and missing fields before they cause runtime issues.
- Use `z.coerce.date()` for date fields so that string dates in frontmatter are automatically parsed into `Date` objects.
- Filter drafts at query time with `getCollection('blog', ({ data }) => !data.draft)` rather than conditionally in templates.
- Keep content files (Markdown/MDX) focused on content. Move complex logic into Astro components or layouts rather than embedding it in MDX.
- Use `reference()` for cross-collection relationships instead of raw string IDs to get type checking on those links.
- **Forgetting to export `collections`**: The `src/content.config.ts` file must export a `collections` object. Without it, Astro ignores your schemas.
- **Schema mismatch crashes the build**: If any content file violates the schema, the entire build fails. Use `.optional()` and `.default()` generously for fields that are not always present.
- **MDX component imports must be relative**: Inside `.mdx` files, component import paths are relative to the MDX file itself, not to the project root.
- **Confusing `id` with `slug`**: In Astro v5+ with loaders, entries use `id` as the identifier. The older `slug` field from v4 collections is no longer automatically generated.
## Quick Example
```bash
npx astro add mdx
```skilldb get astro-skills/Astro Content CollectionsFull skill: 295 linesContent Collections — Astro
You are an expert in Astro content collections for managing structured content with type-safe schemas, Markdown, and MDX.
Overview
Content collections are Astro's built-in way to organize, validate, and query local content such as blog posts, documentation pages, or product data. Collections live in src/content/, each defined by a schema that enforces frontmatter structure at build time. This gives you full TypeScript autocompletion and catches errors before they reach production.
Core Concepts
Defining Collections
Collections are configured in src/content.config.ts (Astro v5+) using defineCollection and Zod schemas:
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
const authors = defineCollection({
loader: glob({ pattern: '**/*.json', base: './src/content/authors' }),
schema: z.object({
name: z.string(),
bio: z.string(),
avatar: z.string().url(),
}),
});
export const collections = { blog, authors };
Content File Format
A typical Markdown content file with validated frontmatter:
---
title: "Getting Started with Astro"
description: "A guide to building your first Astro site"
pubDate: 2026-01-15
tags: ["astro", "tutorial"]
---
## Introduction
Astro makes building fast websites straightforward...
Querying Collections
Use getCollection and getEntry to fetch content:
---
import { getCollection, getEntry } from 'astro:content';
// Get all non-draft blog posts, sorted by date
const posts = (await getCollection('blog', ({ data }) => !data.draft))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
// Get a single entry by ID
const featuredPost = await getEntry('blog', 'my-first-post');
---
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString()}
</time>
</li>
))}
</ul>
Rendering Content
Use the render() function to get the compiled HTML and metadata from a content entry:
---
import { getEntry, render } from 'astro:content';
const post = await getEntry('blog', 'my-first-post');
const { Content, headings, remarkPluginFrontmatter } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
MDX Support
Install the MDX integration to use components inside content files:
npx astro add mdx
Then use components in .mdx files:
---
title: "Interactive Post"
description: "A post with embedded components"
pubDate: 2026-02-10
---
import Chart from '../../components/Chart.jsx';
import Callout from '../../components/Callout.astro';
# Interactive Post
Here is a chart rendered as an island:
<Chart client:visible data={[10, 20, 30]} />
<Callout type="warning">
Remember to add a client directive for interactive components.
</Callout>
Implementation Patterns
Generating Static Pages from a Collection
---
// src/pages/blog/[...slug].astro
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map(post => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<html>
<head><title>{post.data.title}</title></head>
<body>
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
</body>
</html>
Referencing Between Collections
Use reference() to create typed cross-collection references:
// src/content.config.ts
import { defineCollection, z, reference } from 'astro:content';
import { glob } from 'astro/loaders';
const authors = defineCollection({
loader: glob({ pattern: '**/*.json', base: './src/content/authors' }),
schema: z.object({
name: z.string(),
bio: z.string(),
}),
});
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
author: reference('authors'),
relatedPosts: z.array(reference('blog')).default([]),
}),
});
export const collections = { authors, blog };
Resolve references when querying:
---
import { getEntry } from 'astro:content';
const post = await getEntry('blog', 'my-post');
const author = await getEntry(post.data.author);
---
<p>Written by {author.data.name}</p>
Image Handling in Collections
Use the image() helper to validate and optimize images referenced in frontmatter:
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: ({ image }) =>
z.object({
title: z.string(),
cover: image().refine(img => img.width >= 800, {
message: 'Cover image must be at least 800px wide',
}),
}),
});
Custom Remark and Rehype Plugins
Add plugins to transform Markdown/MDX content:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
export default defineConfig({
markdown: {
remarkPlugins: [remarkToc],
rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings],
shikiConfig: {
theme: 'github-dark',
},
},
});
Core Philosophy
Content collections embody Astro's belief that content should be structured, validated, and queryable as a first-class concern, not an afterthought bolted onto a rendering layer. By defining schemas in TypeScript with Zod, you treat your content files the same way you treat database records: every field has a type, every entry is validated, and errors surface at build time rather than in production.
This approach enforces a clean separation between content authorship and presentation logic. Writers work in Markdown or MDX, focusing on prose and frontmatter metadata. Developers define schemas and build query pipelines that filter, sort, and paginate entries. Neither side needs to understand the other's concerns in detail, which scales well as the content corpus grows and multiple people contribute.
The collection system also encourages you to model relationships explicitly using typed references rather than ad-hoc string conventions. When a blog post references an author, that reference is validated against the authors collection at build time. This eliminates broken links and stale references, giving you the data integrity guarantees of a relational database without the operational overhead of running one.
Anti-Patterns
-
Skipping schema definitions for "simple" collections. Even a collection of three Markdown files benefits from a Zod schema. Without it, you lose type inference in queries and allow frontmatter typos to silently propagate to the rendered site.
-
Embedding application logic in MDX content files. MDX allows importing components, but complex conditional rendering, data fetching, or stateful logic inside
.mdxfiles blurs the line between content and code. Keep MDX lean and push logic into Astro layouts or components. -
Using raw string IDs instead of
reference()for cross-collection links. Hard-coded ID strings are fragile. If the referenced entry is renamed or deleted, the build succeeds silently with a broken link. Typed references catch these problems immediately. -
Querying the entire collection when you need a single entry. Calling
getCollection()and filtering in JavaScript for one item wastes build-time resources. UsegetEntry()when you know the specific ID, and reservegetCollection()for list pages and feeds. -
Overloading frontmatter with rendering instructions. Frontmatter should describe the content (title, date, tags), not dictate how it is displayed (layout variant, column count, animation style). Presentation decisions belong in Astro components that consume the content data.
Best Practices
- Always define a Zod schema for every collection. The build-time validation catches typos and missing fields before they cause runtime issues.
- Use
z.coerce.date()for date fields so that string dates in frontmatter are automatically parsed intoDateobjects. - Filter drafts at query time with
getCollection('blog', ({ data }) => !data.draft)rather than conditionally in templates. - Keep content files (Markdown/MDX) focused on content. Move complex logic into Astro components or layouts rather than embedding it in MDX.
- Use
reference()for cross-collection relationships instead of raw string IDs to get type checking on those links.
Common Pitfalls
- Forgetting to export
collections: Thesrc/content.config.tsfile must export acollectionsobject. Without it, Astro ignores your schemas. - Schema mismatch crashes the build: If any content file violates the schema, the entire build fails. Use
.optional()and.default()generously for fields that are not always present. - MDX component imports must be relative: Inside
.mdxfiles, component import paths are relative to the MDX file itself, not to the project root. - Confusing
idwithslug: In Astro v5+ with loaders, entries useidas the identifier. The olderslugfield from v4 collections is no longer automatically generated. - Large collections slowing builds: Each content entry is processed individually. For very large collections (thousands of entries), consider pagination and incremental builds to keep build times manageable.
Install this skill directly: skilldb add astro-skills
Related Skills
Astro Basics
Astro fundamentals including project structure, components, islands architecture, and templating syntax
Astro Deployment
Deploying Astro sites to Vercel, Netlify, Cloudflare Pages, and other platforms
Astro Integrations
Using React, Vue, Svelte, and other UI framework islands within Astro pages
Astro Middleware
Middleware patterns in Astro for authentication, request modification, response headers, and shared context
Astro Routing
File-based and dynamic routing in Astro including static paths, rest parameters, and route priority
Astro SSR
Server-side rendering in Astro with adapters for Node, Vercel, Netlify, Cloudflare, and Deno