Complete Astro Content Collections Guide | Type-Safe Schema, MDX & Blog Building

Complete Astro Content Collections Guide | Type-Safe Schema, MDX & Blog Building

Key Takeaways

Complete guide to building a type-safe content management system with Astro Content Collections. Covers schema definition, MDX, blog, i18n, and SEO with practical examples.

Real-World Experience: Sharing experience building a tech blog platform with Astro Content Collections, achieving 100% type safety and completely eliminating content management errors.

Introduction: “Markdown Management is Complex”

Real-World Problem Scenarios

Scenario 1: Frontmatter Typos
Typos in YAML frontmatter are only discovered at runtime. Content Collections validate at build time.

Scenario 2: Lack of Type Safety
No types when using markdown data. Content Collections auto-generate types.

Scenario 3: Complex Queries
Difficult to filter content with multiple conditions. Content Collections provide powerful query API.


1. What are Content Collections?

Core Concepts

Content Collections is Astro’s type-safe content management system.

Key Advantages:

  • Type Safety: Validate frontmatter with Zod schema
  • Auto Type Generation: Automatically generate TypeScript types
  • Powerful Queries: Filtering, sorting, pagination
  • MDX Support: Use React components
  • Build-time Validation: Catch errors early

2. Basic Setup

Directory Structure

Here’s an implementation example using code. Try running the code directly to see how it works.

src/
└── content/
    ├── config.ts
    ├── blog/
    │   ├── post-1.md
    │   └── post-2.mdx
    └── docs/
        ├── intro.md
        └── guide.md

Schema Definition

Here’s a detailed implementation using TypeScript. Import necessary modules. Please review the code to understand the role of each part.

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

const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    updatedDate: z.date().optional(),
    author: z.string().default('Anonymous'),
    tags: z.array(z.string()),
    draft: z.boolean().default(false),
    featured: z.boolean().default(false),
  }),
});

const docsCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    order: z.number(),
    category: z.enum(['guide', 'api', 'tutorial']),
  }),
});

export const collections = {
  blog: blogCollection,
  docs: docsCollection,
};

3. Writing Content

Markdown

Here’s an implementation example using Markdown. Please review the code to understand the role of each part.

---
title: 'Getting Started with Astro'
description: 'Learn how to build fast websites with Astro'
pubDate: 2026-04-11
author: 'JB'
tags: ['astro', 'tutorial']
draft: false
featured: true
---

# Getting Started

Astro is a modern static site generator...

MDX (Using Components)

Here’s a detailed implementation using MDX. Import necessary modules. Please review the code to understand the role of each part.

---
title: 'Interactive Guide'
description: 'Learn with interactive examples'
pubDate: 2026-04-11
tags: ['interactive']
---

import Button from '@/components/Button.astro';
import Counter from '@/components/Counter.tsx';

# Interactive Guide

Click the button below:

<Button>Click me</Button>

Try the counter:

<Counter client:load />

4. Querying Content

Get All

Here’s a detailed implementation using Astro. Import necessary modules, perform tasks efficiently with async processing, process data with loops. Please review the code to understand the role of each part.

---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';

const allPosts = await getCollection('blog');
const publishedPosts = allPosts.filter(post => !post.data.draft);
---

<h1>Blog Posts</h1>
<ul>
  {publishedPosts.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>{post.data.title}</a>
    </li>
  ))}
</ul>

Filtering and Sorting

Here’s a detailed implementation using Astro. Import necessary modules, perform tasks efficiently with async processing. Please review the code to understand the role of each part.

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

// Filtering
const featuredPosts = await getCollection('blog', ({ data }) => {
  return data.featured && !data.draft;
});

// Sorting
const sortedPosts = featuredPosts.sort((a, b) => 
  b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);

// Filter by tag
const tag = Astro.params.tag;
const tagPosts = await getCollection('blog', ({ data }) => {
  return data.tags.includes(tag);
});
---

Get Single Entry

Here’s a detailed implementation using Astro. Import necessary modules, perform tasks efficiently with async processing, ensure stability with error handling, handle branching with conditionals. Please review the code to understand the role of each part.

---
// src/pages/blog/[...slug].astro
import { getEntry } from 'astro:content';

const { slug } = Astro.params;
const post = await getEntry('blog', slug);

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

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

<article>
  <h1>{post.data.title}</h1>
  <p>By {post.data.author} on {post.data.pubDate.toLocaleDateString()}</p>
  <Content />
</article>

5. Dynamic Routing

getStaticPaths

Here’s a detailed implementation using Astro. Import necessary modules, perform tasks efficiently with async processing, process data with loops. Please review the code to understand the role of each part.

---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';

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

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

<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

Tag Pages

Here’s a detailed implementation using Astro. Import necessary modules, perform tasks efficiently with async processing, process data with loops. Please review the code to understand the role of each part.

---
// src/pages/blog/tag/[tag].astro
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  const allTags = [...new Set(posts.flatMap(post => post.data.tags))];
  
  return allTags.map(tag => ({
    params: { tag },
    props: {
      posts: posts.filter(post => post.data.tags.includes(tag)),
    },
  }));
}

const { tag } = Astro.params;
const { posts } = Astro.props;
---

<h1>Posts tagged with "{tag}"</h1>
<ul>
  {posts.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>{post.data.title}</a>
    </li>
  ))}
</ul>

6. Pagination

Here’s a detailed implementation using Astro. Import necessary modules, perform tasks efficiently with async processing, process data with loops. Please review the code to understand the role of each part.

---
// src/pages/blog/[...page].astro
import { getCollection } from 'astro:content';
import type { GetStaticPaths } from 'astro';

export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
  const posts = await getCollection('blog', ({ data }) => !data.draft);
  const sortedPosts = posts.sort((a, b) => 
    b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
  );
  
  return paginate(sortedPosts, { pageSize: 10 });
};

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

<h1>Blog Posts</h1>
<ul>
  {page.data.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>{post.data.title}</a>
    </li>
  ))}
</ul>

<nav>
  {page.url.prev && <a href={page.url.prev}>Previous</a>}
  <span>Page {page.currentPage} of {page.lastPage}</span>
  {page.url.next && <a href={page.url.next}>Next</a>}
</nav>

Here’s a detailed implementation using Astro. Import necessary modules, perform tasks efficiently with async processing, ensure stability with error handling, process data with loops. Please review the code to understand the role of each part.

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

const { slug } = Astro.params;
const post = await getEntry('blog', slug);
const allPosts = await getCollection('blog', ({ data }) => !data.draft);

// Find posts with same tags
const relatedPosts = allPosts
  .filter(p => 
    p.slug !== slug && 
    p.data.tags.some(tag => post.data.tags.includes(tag))
  )
  .slice(0, 3);
---

<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

<aside>
  <h2>Related Posts</h2>
  <ul>
    {relatedPosts.map(related => (
      <li>
        <a href={`/blog/${related.slug}`}>{related.data.title}</a>
      </li>
    ))}
  </ul>
</aside>

8. RSS Feed Generation

Here’s a detailed implementation using TypeScript. Import necessary modules, perform tasks efficiently with async processing, process data with loops. Please review the code to understand the role of each part.

// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';

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

9. Internationalization Support

Here’s an implementation example using TypeScript. Please review the code to understand the role of each part.

// src/content/config.ts
const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    lang: z.enum(['en', 'ko', 'ja']).default('en'),
    translationKey: z.string().optional(),
  }),
});

Here’s a detailed implementation using Astro. Perform tasks efficiently with async processing, ensure stability with error handling, process data with loops. Please review the code to understand the role of each part.

---
// Find other language versions of same post
const currentPost = await getEntry('blog', slug);
const allPosts = await getCollection('blog');

const translations = allPosts.filter(post => 
  post.data.translationKey === currentPost.data.translationKey &&
  post.slug !== slug
);
---

<nav>
  {translations.map(translation => (
    <a href={`/blog/${translation.slug}`}>
      {translation.data.lang.toUpperCase()}
    </a>
  ))}
</nav>

10. Real-World Example: Tech Blog

Here’s a detailed implementation using TypeScript. Import necessary modules. Please review the code to understand the role of each part.

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

const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    updatedDate: z.date().optional(),
    author: z.string(),
    category: z.enum(['frontend', 'backend', 'devops', 'ai']),
    tags: z.array(z.string()),
    draft: z.boolean().default(false),
    featured: z.boolean().default(false),
    coverImage: z.string().optional(),
    readingTime: z.number(),
    relatedPosts: z.array(z.string()).optional(),
  }),
});

export const collections = { blog: blogCollection };

Here’s a detailed implementation using Astro. Import necessary modules, perform tasks efficiently with async processing, process data with loops. Please review the code to understand the role of each part.

---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import Layout from '@/layouts/Layout.astro';

const allPosts = await getCollection('blog', ({ data }) => !data.draft);
const sortedPosts = allPosts.sort((a, b) => 
  b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);

const featuredPosts = sortedPosts.filter(post => post.data.featured).slice(0, 3);
const recentPosts = sortedPosts.slice(0, 10);

const categories = [...new Set(allPosts.map(post => post.data.category))];
---

<Layout title="Blog">
  <section>
    <h2>Featured Posts</h2>
    <div class="grid">
      {featuredPosts.map(post => (
        <article class="card">
          {post.data.coverImage && (
            <img src={post.data.coverImage} alt={post.data.title} />
          )}
          <h3>
            <a href={`/blog/${post.slug}`}>{post.data.title}</a>
          </h3>
          <p>{post.data.description}</p>
          <div class="meta">
            <span>{post.data.category}</span>
            <span>{post.data.readingTime} min</span>
          </div>
        </article>
      ))}
    </div>
  </section>

  <section>
    <h2>Recent Posts</h2>
    <ul>
      {recentPosts.map(post => (
        <li>
          <a href={`/blog/${post.slug}`}>{post.data.title}</a>
          <time>{post.data.pubDate.toLocaleDateString()}</time>
        </li>
      ))}
    </ul>
  </section>

  <aside>
    <h2>Categories</h2>
    <ul>
      {categories.map(category => (
        <li>
          <a href={`/blog/category/${category}`}>{category}</a>
        </li>
      ))}
    </ul>
  </aside>
</Layout>

Summary and Checklist

Key Summary

  • Content Collections: Astro’s type-safe content management
  • Zod Schema: Frontmatter validation
  • Auto Type Generation: Automatically generate TypeScript types
  • Powerful Queries: Filtering, sorting, pagination
  • MDX Support: Use React components

Implementation Checklist

  • Configure Content Collections
  • Define schema
  • Write content
  • Implement dynamic routing
  • Implement pagination
  • Generate RSS feed
  • Optimize SEO

  • Complete Astro Blog Guide
  • Complete Next.js 15 Guide
  • Complete MDX Guide

Keywords Covered

Astro, Content Collections, MDX, TypeScript, Blog, CMS, Static Site

Frequently Asked Questions (FAQ)

Q. Content Collections vs regular markdown, which is better?

A. Content Collections provide type safety, schema validation, and powerful query API. Regular markdown is sufficient for small projects, but Content Collections are recommended for medium to large projects.

Q. Should I use MDX?

A. Use MDX if you need interactive components. If you only have simple text, regular markdown is faster.

Q. Can I integrate with CMS?

A. Yes, integration with Headless CMS like Contentful and Sanity is possible. However, the type safety benefits of Content Collections are reduced.

Q. How is the performance?

A. Since all content is processed at build time, runtime performance is very fast. No problem even with thousands of content pieces.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3