Skip to main content
Technology & EngineeringAstro295 lines

Astro Content Collections

Content collections in Astro for managing Markdown, MDX, JSON, and YAML content with type-safe schemas

Quick Summary21 lines
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 lines
Paste into your CLAUDE.md or agent config

Content 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 .mdx files 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. Use getEntry() when you know the specific ID, and reserve getCollection() 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 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.

Common Pitfalls

  • 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.
  • 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

Get CLI access →