General5 min read

Understanding Content Collections in Astro

Learn how Astro Content Collections provide strict schema validation, type safety, and efficient querying for content-heavy sites.

E

Listen to Article

Click play to listen to audio narration

Understanding Content Collections in Astro

Content Collections are the recommended way to manage Markdown and MDX content in Astro. They provide strict Zod schema validation and type-safe content queries.


Defining a Collection Schema

In src/content.config.ts, define your content structure using Zod schemas:

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(),
  }),
});

export const collections = { blog };

Querying Content in Pages

Querying posts is straightforward and fully typed:

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

const posts = await getCollection('blog');
---
<ul>
  {posts.map(post => (
    <li>
      <a href={`/blog/${post.id}/`}>{post.data.title}</a>
    </li>
  ))}
</ul>

Benefits of Content Collections

  • Type Safety: Automatic TypeScript inference for frontmatter fields.
  • Build-Time Errors: Missing or invalid frontmatter triggers explicit build errors.
  • Performance: Optimized data loading for large content repositories.

What Are Content Collections?

A content collection is a directory inside src/content/ that Astro manages automatically. Astro generates TypeScript types for your frontmatter, validates entries against a schema at build time, and provides a type-safe API to query your content.

Without content collections, you would manually parse Markdown frontmatter and hope it matches what your templates expect. With collections, if a required field is missing or has the wrong type, the build fails with a clear error.

Defining a Collection

Create src/content.config.ts (or .js):

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

const blog = defineCollection({
  loader: glob({
    base: './src/content/blog',
    pattern: '**/*.{md,mdx}',
  }),
  schema: z.object({
    title: z.string(),
    description: z.string().optional(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

Querying Collections

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

// Get all published posts
const posts = await getCollection('blog', ({ data }) => !data.draft);

// Get a single post by slug
const post = await getEntry('blog', 'getting-started-with-astro-5');
---
<ul>
  {posts.map(post => (
    <li>
      <a href={`/blog/${post.id}`}>{post.data.title}</a>
    </li>
  ))}
</ul>

Rendering Collection Entries

To render the body of a collection entry (convert Markdown/MDX to HTML), use the render() function:

---
import { render } from 'astro:content';
const post = Astro.props.post;
const { Content, headings } = await render(post);
---
<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

headings is an array of all headings in the document β€” useful for generating a Table of Contents.

Using Glob Patterns

The glob loader accepts an array of patterns, which supports exclusion:

loader: glob({
  base: './src/content/blog',
  pattern: [
    '**/*.{md,mdx}',
    '!draft-*.mdx',       // Exclude drafts
    '!**/internal/*.md',  // Exclude internal notes
  ],
}),

Dynamic Routes with Collections

Generate one page per collection entry using Astro’s static path generation:

---
// src/pages/blog/[...slug].astro
import { getCollection, render } 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 render(post);
---
<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

Schema Validation at Build Time

If a required frontmatter field is missing or has the wrong type, astro build fails immediately:

error  [content] Invalid frontmatter in "blog/my-post.mdx"
  - pubDate: Required

This eliminates an entire class of runtime errors from type mismatches between content and templates.

Custom Loaders (Astro 5)

Content Layer in Astro 5 supports custom loaders β€” fetch from any source:

import { defineCollection } from 'astro:content';

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

Written by Editorial Team

Tech contributor covering software architecture, AI research, cloud infrastructure, and systems engineering practices.

View Profile
Recommended For You

Related Articles

Quick:
↑ ↓ Navigate↡ Select
Loading search index...