Skip to main content

Overview

Astro provides powerful content management capabilities through Content Collections, allowing you to organize, validate, and query your content with full TypeScript support.

Content Collections

Content Collections are the recommended way to manage content in Astro. They provide type-safety, validation, and optimized queries for your Markdown, MDX, and data files.

Setting Up a Collection

1

Create the content directory

Create a src/content/ directory and add a subdirectory for your collection:
src/
└── content/
    └── blog/
        ├── post-1.md
        ├── post-2.md
        └── post-3.mdx
2

Define the collection schema

Create src/content.config.ts to define and validate your content:
src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ 
    base: './src/content/blog', 
    pattern: '**/*.{md,mdx}' 
  }),
  schema: ({ image }) => z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: z.optional(image()),
  }),
});

export const collections = { blog };
3

Query your content

Use the type-safe getCollection() function to retrieve your content:
src/pages/blog/index.astro
---
import { getCollection } from 'astro:content';

const posts = await getCollection('blog');
const sortedPosts = posts.sort(
  (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---

<ul>
  {sortedPosts.map((post) => (
    <li>
      <a href={`/blog/${post.id}`}>{post.data.title}</a>
    </li>
  ))}
</ul>

Dynamic Routes with Collections

Generate pages dynamically from your content using getStaticPaths():
src/pages/blog/[...slug].astro
---
import { type CollectionEntry, getCollection, render } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { slug: post.id },
    props: post,
  }));
}

type Props = CollectionEntry<'blog'>;

const post = Astro.props;
const { Content } = await render(post);
---

<BlogPost {...post.data}>
  <Content />
</BlogPost>

Frontmatter

Frontmatter allows you to add metadata to your Markdown and MDX files using YAML syntax.

Basic Frontmatter

src/content/blog/my-post.md
---
title: 'My First Blog Post'
description: 'This is my first post on my new Astro blog.'
pubDate: 2024-01-15
author: 'Jane Doe'
tags: ['astro', 'blogging', 'web development']
---

This is the content of my blog post...

Accessing Frontmatter

When using Content Collections, frontmatter is available through the data property:
---
import { getCollection } from 'astro:content';

const posts = await getCollection('blog');
---

{posts.map((post) => (
  <article>
    <h2>{post.data.title}</h2>
    <p>{post.data.description}</p>
    <time>{post.data.pubDate.toLocaleDateString()}</time>
  </article>
))}

Content Loaders

Content Collections support multiple loader types for flexible content sources.
Load content from your filesystem:
src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const docs = defineCollection({
  loader: glob({ 
    base: './src/content/docs',
    pattern: '**/*.md'
  }),
  schema: z.object({
    title: z.string(),
    order: z.number(),
  }),
});

export const collections = { docs };
Organize different content types with separate collections:
src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ base: './src/content/blog', pattern: '**/*.md' }),
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
  }),
});

const newsletter = defineCollection({
  loader: glob({ base: './src/content/newsletter', pattern: '**/*.md' }),
  schema: z.object({
    subject: z.string(),
    sendDate: z.coerce.date(),
  }),
});

export const collections = { blog, newsletter };

Schema Validation

Use Zod schemas to validate and type-check your content.

Common Schema Patterns

String Fields

title: z.string(),
slug: z.string().regex(/^[a-z0-9-]+$/),
email: z.string().email(),

Numbers & Dates

order: z.number(),
rating: z.number().min(1).max(5),
pubDate: z.coerce.date(),

Optional Fields

subtitle: z.string().optional(),
updatedDate: z.coerce.date().optional(),
tags: z.array(z.string()).default([]),

Images

schema: ({ image }) => z.object({
  coverImage: image(),
  thumbnail: z.optional(image()),
})

Custom Validation

Add custom validation logic:
src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ base: './src/content/blog', pattern: '**/*.md' }),
  schema: z.object({
    title: z.string().min(10, 'Title must be at least 10 characters'),
    draft: z.boolean().default(false),
    pubDate: z.coerce.date(),
  }).refine(
    (data) => data.draft || data.pubDate <= new Date(),
    { message: 'Published posts must have a past publish date' }
  ),
});

export const collections = { blog };

Filtering and Sorting

Query and transform your content with JavaScript:
src/pages/blog/index.astro
---
import { getCollection } from 'astro:content';

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

// Sort by date (newest first)
const sortedPosts = allPosts.sort(
  (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);

// Filter by tag
const tagFilter = 'astro';
const filteredPosts = allPosts.filter((post) =>
  post.data.tags?.includes(tagFilter)
);

// Get only the 5 most recent
const recentPosts = sortedPosts.slice(0, 5);
---

Working with MDX

MDX allows you to use JSX components in your Markdown content.

Setup

npm install @astrojs/mdx
astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';

export default defineConfig({
  integrations: [mdx()],
});

Using Components in MDX

src/content/blog/using-mdx.mdx
---
title: 'Using MDX'
pubDate: 2024-01-20
---

import CustomComponent from '../../components/CustomComponent.astro';
import { Card } from '../../components/Card.jsx';

# Hello from MDX!

<CustomComponent message="This is an Astro component" />

<Card title="Interactive Card">
  This card component works inside my MDX content!
</Card>

CMS Integrations

Astro works with headless CMS platforms for content management.

Contentful

Fetch content from Contentful’s API and render it in your Astro pages.

Sanity

Use Sanity’s GROQ queries to pull structured content into Astro.

Strapi

Connect to your Strapi backend API for content management.

WordPress

Use WordPress as a headless CMS via the REST or GraphQL API.

Example: Fetching from a Headless CMS

src/pages/posts/[slug].astro
---
export async function getStaticPaths() {
  const response = await fetch('https://api.example.com/posts');
  const posts = await response.json();

  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
---

<article>
  <h1>{post.title}</h1>
  <div set:html={post.content} />
</article>

Best Practices

1

Use Content Collections

Always prefer Content Collections over manual file parsing for better type safety and performance.
2

Validate Your Schema

Define strict schemas to catch content errors at build time rather than runtime.
3

Organize Collections Logically

Create separate collections for different content types (blog, docs, products, etc.).
4

Optimize Images

Use the image() schema helper to ensure images are optimized automatically.

Build docs developers (and LLMs) love