Skip to main content
Astro content collections provide a type-safe way to manage content like blog posts, documentation, and other markdown-based content.

What are Content Collections?

Content collections are Astro’s built-in solution for organizing and querying content. They provide:
  • Type Safety - Automatic TypeScript types generated from your schema
  • Validation - Frontmatter is validated against a Zod schema at build time
  • Query API - Simple, powerful API to fetch and filter content
  • Performance - Content is optimized and cached during build
Content collections are defined in src/content/config.ts and content files are stored in src/content/<collection-name>/.

Blog Collection Schema

The blog collection is defined in src/content/config.ts with the following schema:
import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    createdAt: z.string().datetime(),
    updatedAt: z.string().datetime().optional(),
    image: z.string(),
    description: z.string(),
    author: z.string(),
    publish: z.boolean(),
    tags: z.array(z.string()).optional(),
    authorImage: z.string(),
  }),
});

export const collections = {
  'blog': blogCollection,
};

Required Fields

Every blog post must include these fields:
  • title - The post title (string)
  • createdAt - ISO 8601 datetime string (e.g., "2025-08-12T04:17:35.590Z")
  • image - URL to the cover image (string)
  • description - Brief post description for SEO and previews (string)
  • author - Author’s name (string)
  • publish - Whether the post is published (boolean)
  • authorImage - URL to author’s avatar image (string)

Optional Fields

  • updatedAt - ISO 8601 datetime string for last update
  • tags - Array of tag strings (e.g., ["Guide", "Tutorial"])
The schema uses Zod’s datetime() validator which ensures dates are in ISO 8601 format. You can generate this format using new Date().toISOString() in JavaScript.

Type Safety Benefits

Once defined, the schema provides automatic TypeScript types:
import { getCollection } from 'astro:content';

const posts = await getCollection('blog');

// TypeScript knows the exact shape of post.data
posts.forEach(post => {
  console.log(post.data.title);      // ✅ Type: string
  console.log(post.data.publish);    // ✅ Type: boolean
  console.log(post.data.tags);       // ✅ Type: string[] | undefined
  console.log(post.data.invalid);    // ❌ TypeScript error!
});

Querying Collections

Get All Posts

import { getCollection } from 'astro:content';

const allPosts = await getCollection('blog');

Filter Published Posts

const publishedPosts = await getCollection('blog', ({ data }) => {
  return data.publish === true;
});

Sort by Date

const sortedPosts = allPosts.sort((a, b) => {
  return new Date(b.data.createdAt).getTime() - 
         new Date(a.data.createdAt).getTime();
});

Get Recent Posts

const recentPosts = allPosts
  .filter(post => post.data.publish === true)
  .sort((a, b) => new Date(b.data.createdAt).getTime() - 
                  new Date(a.data.createdAt).getTime())
  .slice(0, 3);
This exact pattern is used in src/pages/blog/[slug].astro:125-132 to display recent articles in the sidebar.

Rendering Content

To render a blog post’s content:
const post = await getEntry('blog', 'my-post-slug');
const { Content } = await post.render();
Then in your component:
<div class="prose">
  <Content />
</div>

Collection Structure

src/
└── content/
    ├── config.ts              # Schema definition
    └── blog/                  # Blog collection
        ├── guide-to-geometry-dash.md
        ├── another-post.md
        └── ...

File Naming

  • Files can use .md or .mdx extensions
  • The filename (without extension) becomes the slug
  • Hyphens in filenames create URL-friendly slugs
Example:
  • File: guide-to-geometry-dash.md
  • URL: /blog/guide-to-geometry-dash
  • Slug: guide-to-geometry-dash

Validation Errors

If a post’s frontmatter doesn’t match the schema, you’ll get a build error:
 Missing required field: "title"
 Expected string, received number: "publish"
 Invalid datetime format: "createdAt"
This catches errors early, before they reach production.
Validation happens at build time, so you can’t accidentally publish invalid content. This is much safer than runtime validation.

Next Steps

Build docs developers (and LLMs) love