Content Collections
Type-safe, schema-validated Markdown content managed through Astro's content collections API.
읽는 데 2분
A handful of blog posts can live as loose Markdown files, referenced by hand. Once a site has dozens of posts — or docs pages, or product entries — you want two things a loose pile of files doesn't give you: a guarantee that every entry has the frontmatter fields it's supposed to, and a typed way to query them. That's what content collections provide.
Defining a collection
Collections are declared in src/content/config.ts, where a Zod schema describes exactly what frontmatter every entry must have:
// src/content/config.ts
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
publishDate: z.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };Every Markdown file placed in src/content/blog/ is now validated against this schema. If a post is missing title, or has publishDate written as plain text instead of a date, the build fails with a clear error — instead of that mistake silently reaching production as a page with a missing heading.
Writing an entry
---
title: "Why We Switched to Astro"
publishDate: 2026-03-14
tags: ["astro", "performance"]
---
We cut our JavaScript bundle by 90% without rewriting our content.This is an ordinary Markdown file — the schema doesn't change how you write content, only how strictly its frontmatter is checked.
Querying collections
astro:content exposes functions to read entries back, fully typed against the schema you defined:
---
import { getCollection } from "astro:content";
const posts = await getCollection("blog", ({ data }) => !data.draft);
const sorted = posts.sort(
(a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
);
---
<ul>
{sorted.map((post) => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</li>
))}
</ul>getCollection accepts an optional filter — here, excluding drafts — and every returned entry's data is typed according to the Zod schema, so an editor autocompletes post.data.title and flags post.data.titel as an error before you even run the build.
Rendering a single entry's body
Pairing getStaticPaths (from the dynamic routes lesson) with a collection generates one page per entry, rendering each Markdown body via its render() method:
---
// 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();
---
<h1>{post.data.title}</h1>
<Content />post.render() returns a <Content /> component representing the compiled Markdown body, dropped straight into the template. Together, the schema and this query pattern turn a folder of Markdown files into something closer to a lightweight, type-checked database — with none of the runtime cost of an actual database, since it's all resolved at build time.