Astro Content Collections — Type-Safe Markdown That Fails the Build
Astro Content Collections turn a pile of Markdown files into a typed content store. Instead of discovering a missing title in production, the build fails when frontmatter does not match a Zod schema. If you have ever shipped “Untitled” because of a typo’d key, collections are the fix.
Why collections beat raw import.meta.glob
Globbing Markdown works until you need consistent dates, tags, and descriptions across a hundred posts. Collections give you:
- Schema validation at build time
- Generated TypeScript types for frontmatter
- Helpers like
getCollection/getEntrywith filters - A clear place to put content config next to the files
You still write Markdown/MDX; you simply stop treating frontmatter as untyped any.
Minimal setup
Put files in src/content/<collection>/ and define the collection in the content config (Astro’s current docs use src/content.config.ts on newer versions — follow the version you run):
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string().min(1),
description: z.string().min(1),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
z.coerce.date() accepts 2025-02-15 strings and turns them into Date objects — fewer custom parsers.
Querying with types
import { getCollection } from "astro:content";
const posts = (await getCollection("blog"))
.filter((p) => !p.data.draft)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
p.data.title is a string; typos fail in the editor. Rendering still uses await render(post) (or the pattern your Astro version documents) to get HTML from the body.
Drafts, locales, and multiple collections
Split by responsibility:
| Collection | Example use |
|---|---|
blog | Long-form posts |
blog-en | English locale tree |
docs | Versioned documentation |
changelog | Short dated entries |
Filter draft: true out of production sitemaps. For localization, separate collections or a locale field both work — pick one convention and stick to it so getStaticPaths stays readable.
MDX, images, and references
MDX collections can allow components in content — constrain what authors may import if you open that door. For images, prefer Astro assets or explicit relative paths validated by convention; put image alt requirements in code review if schema cannot express them fully.
Reference other entries (authors, related posts) with reference() in schemas when you want broken links to fail the build instead of 404 at runtime.
Migration from a loose folder
- Move Markdown into
src/content/blog/. - Add the schema with only the fields you already use.
- Run the build; fix frontmatter errors in batches.
- Tighten the schema (min lengths, enums for tags) once green.
- Replace glob-based pages with
getCollection.
Do not invent fifteen optional fields on day one — validate what you already depend on.
Authoring ergonomics
- Keep a
TEMPLATE.mdwith the required frontmatter - Lint tag spelling via
z.enum([...])when the set is small - Reject empty descriptions — they become poor SEO snippets
- Store dates in ISO form for sorting sanity
Common build failures and fixes
| Error | Fix |
|---|---|
| Date parse failures | Use ISO strings + coerce.date |
| Unexpected key | Strip unknown keys or add to schema |
| Duplicate slugs | Rename files; slug comes from path |
| Draft leaked to prod | Filter in getCollection and sitemaps |
Bottom line
Content Collections make blog management boring in the best way: schemas catch typos, types autocomplete frontmatter, and getCollection replaces hand-rolled globs. Spend an hour on the schema once — every future post inherits the guardrails.
Closing notes
Keep this page next to your incident runbook. The value is a reusable mental model for the next outage or launch — not a checklist you read once and forget. Trim personal notes down to the three steps you actually used last time; short runbooks get followed, long ones get skipped under pressure.