Skip to content
All essays
WebMarch 21, 202414 min

Astro Content Collections: Type-Safe Content Management

Master Astro Content Collections for building type-safe, performant blogs and documentation sites with automatic schema validation and TypeScript support.

Ü
Ümit Uz
Mobile & Full Stack Developer

Content Collections represent one of Astro's most powerful features for managing content-driven websites. They provide a structured, type-safe way to organize and validate your content, eliminating common errors and improving developer experience significantly.

What are Content Collections?

Content Collections allow you to define schemas for your content, enabling Astro to validate your markdown, MDX, or data files at build time. This approach provides:

  • TypeScript Support: Full autocompletion and type checking for your content
  • Schema Validation: Catch content errors before deployment
  • Performance: Optimized data loading and querying
  • Developer Experience: Excellent IDE support with IntelliSense

Setting Up Content Collections

Enable Collections

Create a ts.config.json file to enable TypeScript support:

json
{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  }
}

Astro automatically detects content collections in the src/content/ directory.

Creating Your First Collection

Define the Schema

Create src/content/config.ts to define your collection:

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

const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishDate: z.coerce.date(),
    author: z.string(),
    image: z.string().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
});

export const collections = {
  blog: blogCollection,
};

Create Content Files

Add markdown files to src/content/blog/:

markdown
---
title: "My First Blog Post"
description: "This is my first post using Astro Content Collections"
publishDate: 2024-03-20
author: "Jane Developer"
tags: ["astro", "webdev"]
draft: false
---

# My First Blog Post

Welcome to my blog post powered by Astro Content Collections!

Querying Collections

Get All Entries

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

const allPosts = await getCollection('blog');

// Filter out drafts
const publishedPosts = allPosts.filter(post => !post.data.draft);
---
<ol>
  {publishedPosts.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>{post.data.title}</a>
      <time>{post.data.publishDate.toISOString()}</time>
    </li>
  ))}
</ol>

Get Single Entry

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

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

Advanced Schema Validation

Nested Objects

typescript
const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    author: z.object({
      name: z.string(),
      bio: z.string().optional(),
      twitter: z.string().optional(),
    }),
  }),
});

Enums and Choices

typescript
const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    category: z.enum(['tutorial', 'article', 'news']),
    difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
  }),
});

Custom Validation

typescript
const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string().min(10).max(100),
    slug: z.string().regex(/^[a-z0-9-]+$/),
    email: z.string().email(),
  }),
});

Working with Images

Image References

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

const blogCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    heroImage: z.object({
      src: z.string(),
      alt: z.string(),
    }),
    // Reference to another collection
    author: reference('authors'),
  }),
});

Multiple Collections

Blog and Authors

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

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    author: reference('authors'),
  }),
});

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

export const collections = { blog, authors };

Querying with References

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

const posts = await getCollection('blog');
const postsWithAuthors = await Promise.all(
  posts.map(async (post) => {
    const author = await post.data.author();
    return { ...post, author };
  })
);
---

Sorting and Filtering

Sort by Date

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

Filter by Tags

typescript
const posts = await getCollection('blog');
const taggedPosts = posts.filter(post =>
  post.data.tags.includes('typescript')
);
typescript
async function getRelatedPosts(currentPost: CollectionEntry<'blog'>) {
  const posts = await getCollection('blog');
  return posts
    .filter(post => post.id !== currentPost.id)
    .filter(post =>
      post.data.tags.some(tag =>
        currentPost.data.tags.includes(tag)
      )
    )
    .slice(0, 3);
}

Data Collections

JSON and YAML Files

Create src/content/products/product-1.json:

json
{
  "name": "Product 1",
  "price": 99.99,
  "description": "An amazing product"
}

Define the schema:

typescript
const products = defineCollection({
  type: 'data',
  schema: z.object({
    name: z.string(),
    price: z.number(),
    description: z.string(),
  }),
});

Pagination

Paginated Blog Listings

astro
---
import { getCollection } from 'astro:content';
import Pagination from '../../components/Pagination.astro;

export async function getStaticPaths({ paginate }) {
  const posts = await getCollection('blog');
  const publishedPosts = posts.filter(post => !post.data.draft);

  return paginate(publishedPosts, { itemsPerPage: 10 });
}

const { page } = Astro.props;
---
<ol>
  {page.data.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>{post.data.title}</a>
    </li>
  ))}
</ol>

<Pagination page={page} />

RSS Feeds

Generate RSS

typescript
// rss.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';

export async function GET(context) {
  const posts = await getCollection('blog');
  return rss({
    title: 'My Blog',
    description: 'My awesome blog',
    site: context.site,
    items: posts.map(post => ({
      title: post.data.title,
      pubDate: post.data.publishDate,
      description: post.data.description,
      link: `/blog/${post.slug}/`,
    })),
  });
}

Performance Optimization

Reduce Bundle Size

Content Collections are tree-shaken, so unused data doesn't increase your bundle size.

Build-Time Optimization

All data fetching happens at build time, so your runtime performance is optimal.

Best Practices

1. Use Strict Schemas

Define comprehensive schemas to catch errors early:

typescript
schema: z.object({
  required: z.string(),
  optional: z.string().optional(),
  withDefault: z.number().default(0),
})

2. Leverage TypeScript

Use TypeScript throughout for better autocomplete:

typescript
const posts = await getCollection('blog');
posts.forEach(post => {
  // post.data is fully typed
  console.log(post.data.title);
});

3. Organize Content Hierarchically

Use nested directories for organization:

src/content/blog/
  ├── 2024/
  │   ├── 03/
  │   │   ├── post-1.md
  │   │   └── post-2.md
  └── 2024/
      └── 04/
          └── post-3.md

4. Use Frontmatter Defaults

Set default values in your schema:

typescript
schema: z.object({
  draft: z.boolean().default(false),
  tags: z.array(z.string()).default([]),
})

5. Validate External Data

Use collections to validate external data:

typescript
import { defineCollection } from 'astro:content';
import externalData from '../data/external.json';

const validatedData = blogCollection.schema.parse(externalData);

Real-World Example

Complete Blog Setup

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

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string().min(5).max(100),
    description: z.string().min(50).max(200),
    publishDate: z.coerce.date(),
    author: z.object({
      name: z.string(),
      avatar: z.string().url(),
      bio: z.string(),
    }),
    image: z.object({
      src: z.string(),
      alt: z.string(),
    }),
    tags: z.array(z.string()).default([]),
    category: z.enum(['dev', 'design', 'career']),
    draft: z.boolean().default(false),
    featured: z.boolean().default(false),
  }),
});

export const collections = { blog };

Conclusion

Astro Content Collections provide a robust, type-safe foundation for managing content at scale. By defining schemas and leveraging TypeScript, you can build content-driven websites that are both performant and maintainable.

The combination of build-time validation, excellent developer experience, and runtime performance makes Content Collections an essential tool for any serious Astro project.

Next up: Learn how to extend Astro's capabilities with powerful integrations.

Enjoyed this essay? Share it.

Related essays

Next essay
React Hooks Best Practices