본문으로 건너뛰기 Complete Astro Content Collections Complete Guide

Complete Astro Content Collections Complete Guide

Complete Astro Content Collections Complete Guide

이 글의 핵심

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

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

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

Schema Definition

// 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-02-06
author: 'JB'
tags: ['astro', 'tutorial']
draft: false
---
featured: true
# Getting Started
Astro is a modern static site generator...

MDX (Using Components)


title: 'Interactive Guide
description: 'Learn with interactive examples.'
pubDate: 2026-02-06
---
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


// 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


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


// 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


// 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


// 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


// 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>


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

// 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

// 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(),
  }),
});

// 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

// 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 };

// 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.


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Complete guide to building a type-safe content management system with Astro Content Collections. Schema definition, MDX, blog, i18n, SEO with practical …

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Astro, Content Collections, MDX, TypeScript, Blog, CMS, Static Site.