Skip to main content
Content Collections are Astro’s solution for managing content like blog posts, documentation, or any structured data. They provide type-safety, validation, and automatic TypeScript types for your content.

What are Content Collections?

Content Collections organize your content into typed, validated groups. Instead of manually loading markdown files, you define collections with schemas and let Astro handle the rest.
Key Benefits: Type-safe frontmatter, automatic TypeScript types, content validation, and optimized image handling.

Setup

Create a content.config.ts file in your src/ directory:
src/content.config.ts
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(),
    author: z.string(),
    image: z.string().optional(),
  }),
});

export const collections = { blog };
Organize your content:
src/
├── content/
│   └── blog/
│       ├── post-1.md
│       ├── post-2.md
│       └── post-3.mdx
└── content.config.ts

Defining Collections

Use defineCollection() to create a collection:
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blog = defineCollection({
  // Where to load content from
  loader: glob({ 
    base: './src/content/blog', 
    pattern: '**/*.{md,mdx}' 
  }),
  
  // Validate frontmatter
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()).default([]),
  }),
});

export const collections = { blog };
const blog = defineCollection({
  loader: glob({ pattern: '*.md' }),
  schema: z.object({
    title: z.string(),
    date: z.date(),
  }),
});

Schema Validation

Schemas use Zod for runtime validation:
import { z } from 'astro/zod';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md' }),
  schema: z.object({
    // Required string
    title: z.string(),
    
    // Optional string with default
    description: z.string().default('No description'),
    
    // Transform string to Date
    pubDate: z.coerce.date(),
    
    // Optional date
    updatedDate: z.coerce.date().optional(),
    
    // Array with default
    tags: z.array(z.string()).default([]),
    
    // Enum
    status: z.enum(['draft', 'published', 'archived']),
    
    // Boolean with default
    featured: z.boolean().default(false),
    
    // Nested object
    author: z.object({
      name: z.string(),
      email: z.string().email(),
    }),
  }),
});

Image Schemas

Use the image() helper for optimized images:
const blog = defineCollection({
  loader: glob({ pattern: '**/*.md' }),
  schema: ({ image }) => z.object({
    title: z.string(),
    heroImage: image(),
    thumbnail: z.optional(image()),
  }),
});
In your markdown:
src/content/blog/post.md
---
title: My Post
heroImage: ./hero.jpg
thumbnail: ./thumb.png
---

Post content here.

Querying Collections

Get All Entries

Use getCollection() to fetch all entries:
src/pages/blog/index.astro
---
import { getCollection } from 'astro:content';

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

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

Filter Entries

Filter with a callback function:
---
import { getCollection } from 'astro:content';

// Get only published posts
const posts = await getCollection('blog', (entry) => {
  return entry.data.status === 'published';
});

// Get posts by tag
const reactPosts = await getCollection('blog', (entry) => {
  return entry.data.tags.includes('react');
});

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

Get Single Entry

Use getEntry() to fetch a specific entry:
src/pages/blog/[id].astro
---
import { getEntry } from 'astro:content';

const post = await getEntry('blog', Astro.params.id);

if (!post) {
  return Astro.redirect('/404');
}

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

<article>
  <h1>{post.data.title}</h1>
  <time>{post.data.pubDate.toLocaleDateString()}</time>
  <Content />
</article>

Rendering Content

Call render() on an entry to get the content component:
---
import { getEntry } from 'astro:content';

const post = await getEntry('blog', 'my-post');
const { Content, headings, remarkPluginFrontmatter } = await post.render();
---

<article>
  <h1>{post.data.title}</h1>
  
  <!-- Table of contents -->
  <nav>
    {headings.map((heading) => (
      <a href={`#${heading.slug}`}>
        {heading.text}
      </a>
    ))}
  </nav>
  
  <!-- Rendered content -->
  <Content />
</article>
The Content component is the rendered markdown/MDX. headings contains the document outline.

Dynamic Routes

Generate routes from collections:
src/pages/blog/[...slug].astro
---
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';

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

<BlogLayout
  title={post.data.title}
  description={post.data.description}
  pubDate={post.data.pubDate}
>
  <Content />
</BlogLayout>

Loaders

Loaders determine where content comes from. The glob() loader reads files from disk:
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({
    base: './src/content/blog',
    pattern: '**/*.{md,mdx}',
  }),
  schema: z.object({ /* ... */ }),
});
You can also create custom loaders to fetch content from APIs, databases, or CMSs.

References Between Collections

Create relationships between collections:
src/content.config.ts
import { defineCollection, reference } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const authors = defineCollection({
  loader: glob({ pattern: '*.json' }),
  schema: z.object({
    name: z.string(),
    bio: z.string(),
  }),
});

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md' }),
  schema: z.object({
    title: z.string(),
    author: reference('authors'),
  }),
});

export const collections = { blog, authors };
Query referenced entries:
---
import { getEntry } from 'astro:content';

const post = await getEntry('blog', 'my-post');
const author = await getEntry('authors', post.data.author);
---

<article>
  <h1>{post.data.title}</h1>
  <p>By {author.data.name}</p>
</article>

Type Safety

Astro generates TypeScript types automatically:
// Generated types are available globally
import type { CollectionEntry } from 'astro:content';

type BlogPost = CollectionEntry<'blog'>;

function formatPost(post: BlogPost) {
  // post.data is fully typed!
  return {
    title: post.data.title,
    date: post.data.pubDate.toISOString(),
  };
}

Implementation Details

From the source code at src/content/runtime.ts, collections use:
  1. Data Store: Content entries are stored in a global data store
  2. Schema Validation: Zod schemas validate entries at build time
  3. Type Generation: TypeScript types are auto-generated from schemas
// Simplified from src/content/runtime.ts
export function createGetCollection({ liveCollections }) {
  return async function getCollection(
    collection: string,
    filter?: (entry: any) => unknown
  ) {
    const store = await globalDataStore.get();
    
    const result = [];
    for (const rawEntry of store.values(collection)) {
      const data = updateImageReferencesInData(
        rawEntry.data, 
        rawEntry.filePath,
        imageAssetMap
      );
      
      let entry = { ...rawEntry, data, collection };
      
      if (filter && !filter(entry)) {
        continue;
      }
      
      result.push(entry);
    }
    
    return result;
  };
}

Practical Examples

Blog with Categories

src/content.config.ts
const blog = defineCollection({
  loader: glob({ pattern: '**/*.md' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    category: z.enum(['tech', 'design', 'business']),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()).default([]),
  }),
});
src/pages/blog/category/[category].astro
---
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const categories = ['tech', 'design', 'business'];
  
  return categories.map((category) => ({
    params: { category },
  }));
}

const { category } = Astro.params;
const posts = await getCollection('blog', (entry) => {
  return entry.data.category === category;
});
---

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

Documentation with Sidebar

src/content.config.ts
const docs = defineCollection({
  loader: glob({ pattern: '**/*.mdx' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    order: z.number().default(0),
    sidebar: z.object({
      label: z.string(),
      order: z.number(),
    }),
  }),
});
src/components/DocsSidebar.astro
---
import { getCollection } from 'astro:content';

const docs = await getCollection('docs');
const sorted = docs.sort((a, b) => 
  a.data.sidebar.order - b.data.sidebar.order
);
---

<nav>
  <ul>
    {sorted.map((doc) => (
      <li>
        <a href={`/docs/${doc.id}`}>
          {doc.data.sidebar.label}
        </a>
      </li>
    ))}
  </ul>
</nav>

Best Practices

1

Define clear schemas

Use descriptive field names and provide defaults where appropriate.
2

Validate early

Let Zod catch errors at build time, not runtime.
3

Use TypeScript

Take advantage of auto-generated types for type safety.
4

Organize by collection

Group similar content together (blog, docs, authors).
5

Filter in queries

Filter collections at query time for flexibility.

Use enums

Define allowed values with z.enum() for better validation.

Reference related content

Use reference() to create relationships between collections.

Optimize images

Use the image() helper for automatic image optimization.

Default values

Provide sensible defaults to make frontmatter easier to write.

Learn More

Routing

Generate routes from collections

Layouts

Create layouts for your content

Build docs developers (and LLMs) love