Content Collections

Content collections are Astro’s way of managing structured content (Markdown, MDX, YAML, JSON) with schema validation and type safety.

Why content collections

If you have local authored content (blog posts, docs, how-to guides), content collections give you:

  • Schema validation with Zod
  • TypeScript types auto-generated from your schema
  • Querying APIs (getCollection(), getEntry())
  • Frontmatter enforcement (required fields, default values)
  • MDX support via @astrojs/mdx

This replaces the gray-matter + manual file reading pattern common in Next.js projects.

Defining a collection

Create a content.config.ts (or .js) at the project root:

// content.config.ts
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',  // Markdown/MDX content
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    author: z.string().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
});

const authors = defineCollection({
  type: 'data',  // JSON/YAML data
  schema: z.object({
    name: z.string(),
    bio: z.string(),
    avatar: z.string(),
  }),
});

export const collections = { blog, authors };

Collection types

  • content - Markdown, MDX, or Markdoc files that render to HTML
  • data - JSON, YAML, or TOML files that provide structured data

File structure

src/content/
  blog/
    first-post.md
    second-post.mdx
  authors/
    peter.json
    sarah.yaml

Each file’s name (without extension) becomes its id.

Querying collections

---
import { getCollection, getEntry } from 'astro:content';

// Get all entries
const allPosts = await getCollection('blog');

// Filter entries
const publishedPosts = await getCollection('blog', ({ data }) => {
  return !data.draft;
});

// Get single entry
const post = await getEntry('blog', 'first-post');
---

Rendering content

---
import { getEntry } from 'astro:content';

const post = await getEntry('blog', 'first-post');
const { Content } = await post.render();
---
<article>
  <h1>{post.data.title}</h1>
  <time>{post.data.pubDate.toLocaleDateString()}</time>
  <Content />
</article>

Using collections with dynamic routes

---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';

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();
---
<h1>{post.data.title}</h1>
<Content />

Image references in schemas

Content collection schemas can reference and validate images:

import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: ({ image }) => z.object({
    title: z.string(),
    cover: image(),  // validates image exists, enables optimization
  }),
});

Live collections (Astro 6)

For frequently-updated remote data, Astro 6 introduced live collections that fetch at request time:

import { defineCollection, z } from 'astro:content';

const news = defineCollection({
  type: 'content',
  loader: async () => {
    const response = await fetch('https://api.example.com/articles');
    const articles = await response.json();
    return articles.map(article => ({
      id: article.slug,
      body: article.content,
      data: {
        title: article.title,
        pubDate: new Date(article.published_at),
      },
    }));
  },
  schema: z.object({
    title: z.string(),
    pubDate: z.date(),
  }),
});

Caveats of live collections:

  • No MDX support at runtime
  • No image optimization at runtime
  • Performance cost per request (unless cached)

For a news site backed by an existing API, it is often simpler to fetch directly in the page frontmatter rather than using live collections. Reserve content collections for local authored content where schema validation and type safety matter.

MDX integration

Install @astrojs/mdx to use .mdx files in collections:

npx astro add mdx

MDX files can import and use components:

---
title: My Guide
---

import Callout from '../../components/Callout.astro';

# Getting Started

<Callout type="info">
  This is a tip inside MDX.
</Callout>

Regular markdown continues here.

Comparison with Next.js content handling

FeatureNext.jsAstro
Local MarkdownManual (gray-matter + next-mdx-remote or similar)Content collections with built-in schema validation
Type safetyManualAuto-generated from Zod schemas
Queryingfs.readFileSync + import.meta.globgetCollection(), getEntry()
MDX@next/mdx or next-mdx-remote@astrojs/mdx integration
Remote contentFetch in page/layoutFetch in frontmatter or live collections
Image validationManualSchema-level image() helper