Astro Content Collections, Explained
Astro content collections give markdown and MDX files a type-safe schema and a query API, catching bad frontmatter at build time instead of runtime.
Content collections are Astro’s built-in system for organizing groups of markdown, MDX, or data files, validating their frontmatter against a schema, and querying them with a fully typed API instead of hand-rolling file reads and frontmatter parsing. Instead of a folder of loosely structured markdown files that any typo in the frontmatter can silently break, a collection defines exactly what fields each entry must have — and Astro checks every entry against that definition at build time.
The problem plain markdown has
Without collections, a typical approach to a markdown-driven site is to glob a folder of .md files, parse each one’s frontmatter with a library, and hope every file actually has the fields the templates expect. Nothing enforces that pubDate is really a date, that tags is really an array, or that a required field wasn’t simply forgotten on one post. Those mistakes usually surface as a runtime error deep in a template, or worse, a page that silently renders with undefined where a title should be.
This is precisely the failure mode content collections are built to catch before a page ever ships — a bad or missing field fails the build instead of failing silently.
Defining a collection
A collection is declared in src/content.config.ts, pairing a loader — which tells Astro where the entries live and how to read them — with a schema — which describes and validates the shape of each entry’s data:
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const blog = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
}),
});
export const collections = { blog };
The schema is built with Zod, which does double duty: it validates every entry’s frontmatter at build time, and it infers a matching TypeScript type automatically, so every place the collection is queried in the codebase gets full autocomplete and type-checking on title, pubDate, and tags with zero manual type definitions to keep in sync.
Loaders: not just local markdown
Astro 5’s content layer generalized what a collection’s source can be. The glob loader used above reads local files matching a pattern, but a loader is really just a function that returns entries — which means a collection can just as easily be backed by a file loader reading a single JSON or YAML file, or a custom loader that fetches from a CMS API, a database, or any other remote source, all normalized behind the same getCollection() query interface. A site can mix a glob-backed blog collection with a file-backed collection of static reference data — an authors list, a set of product SKUs — and query both the same way.
Querying entries
Once defined, a collection is queried with getCollection() and getEntry(), both of which return fully typed data:
import { getCollection } from "astro:content";
const posts = await getCollection("blog", ({ data }) => !data.draft);
const sorted = posts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
The optional filter callback on getCollection() runs against already-validated, already-typed data — there’s no need to guard against a missing field or parse a date string, because the schema already guaranteed both by the time this code runs. Rendering an individual entry’s markdown body works through a separate render() call, which returns the compiled <Content /> component along with the page’s heading structure, useful for building a table of contents.
Content collections vs a headless CMS vs raw frontmatter
| Raw frontmatter parsing | Content collections | Headless CMS | |
|---|---|---|---|
| Schema validation | None (manual) | Built in, fails the build on error | Enforced by the CMS |
| Type safety | None, unless hand-written | Inferred automatically from the schema | Depends on generated types, if any |
| Data source | Local files only | Local files, remote APIs, or both, via loaders | Remote API only |
| Editing experience | Any text editor | Any text editor (or Git-based CMS on top) | Dedicated editor UI |
| Build-time failure on bad data | No | Yes | Depends on the integration |
Collections sit in a useful middle ground: all the type safety and validation of a schema-driven system, without requiring a separate service or database the way a headless CMS does. For a documentation site or blog maintained by developers directly in Git, that’s usually the better trade — for a marketing team that needs a visual editor and no comfort with markdown, a headless CMS (potentially wired in as a custom loader) is still the right call.
Where this fits in an Astro build
Because collections are resolved at build time, a validation failure shows up as a build error rather than a production bug — the same discipline that makes deploying an Astro site to Cloudflare Pages predictable: if the build passes locally, the content that shipped is schema-valid by construction. This pairs naturally with Astro’s default of shipping static HTML — see islands architecture for how that default extends to interactivity — and with static-site search, which typically indexes exactly the validated collection output rather than raw files.
The takeaway
Content collections turn a folder of markdown files from an untyped grab bag into a schema-validated, fully typed data source, catching malformed or missing frontmatter at build time instead of at runtime. The glob loader and Zod schema shown here cover the common case of a local markdown blog, but the same getCollection() API extends to remote and structured data sources through custom loaders — one query interface regardless of where the entries actually live.
Keep reading
Takina · · 4 min read Astro Islands Architecture Explained
Astro's islands architecture ships static HTML by default and hydrates only the interactive components that need JavaScript. Here's how it works.
Takina · · 4 min read CSS @starting-style Explained: Entry Transitions in CSS
CSS @starting-style defines an element's before-state on first render, so transitions can animate elements as they appear, even from display: none.
Takina · · 5 min read Event Bubbling vs Capturing in JavaScript, Explained
Event bubbling and capturing are the two directions a DOM event travels. How propagation phases work, when to use each, and how to stop them.