Astro's content collections API gives you a type-safe way to organise and query local Markdown, MDX, and data files without reaching for a full CMS. Instead of scattering front-matter conventions across a folder of loose files and hoping they stay consistent, a collection is defined by a schema, validated at build time, and queried through a typed API — errors in a post's front matter surface as build failures instead of silent runtime bugs.
Defining a collection and its schema
Collections live under src/content/, and each collection's shape is declared in src/content/config.ts using Zod schemas. A blog collection might require a title, publish date, and optional tags array; Astro validates every entry in the collection against that schema when you run astro build or astro dev, so a missing required field or a wrong type fails immediately rather than producing a broken page at request time.
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
Querying with getCollection and getEntry
Once a collection is defined, getCollection('blog') returns every validated entry with full TypeScript autocomplete on the front matter fields, and getEntry('blog', slug) fetches a single entry. Filtering happens in plain JavaScript — getCollection('blog', ({ data }) => !data.draft) excludes drafts from a production build without a separate query language. This is the piece that replaces a lot of ad-hoc glob-and-parse logic teams used to write against raw Markdown files.
The Content Layer API and remote sources
Since Astro 4.14, the Content Layer API extends collections beyond local files: a loader can pull entries from a remote API, a headless CMS, or a database, and the results are cached and validated the same way as local Markdown. This is the piece that matters if content genuinely lives outside the repo — you get the same typed query surface (getCollection, getEntry) regardless of where the data originated, instead of writing a bespoke fetch-and-cache layer per source.
A custom loader implements a load() function that populates the collection's data store; the schema validation and query API stay identical whether the loader reads local files or calls a remote endpoint. This makes swapping a CMS backend later a loader change, not a template rewrite.
Generating routes from a collection
Static routes are generated the standard Astro way: a dynamic route file (src/pages/blog/[slug].astro) calls getStaticPaths, which maps over getCollection('blog') and returns a params/props pair per entry. Each entry's render() function returns the compiled content plus a table-of-contents-friendly heading list, so the page component gets rendered Markdown/MDX output without a separate render call.
Tightening a schema — making an optional field required, or narrowing a type — will fail the build on any existing entry that doesn't conform. Treat schema changes on a collection with real content the same as a database migration: check what's already there before you ship the change.
When to reach for a headless CMS instead
Content collections are the right fit when content is authored by developers or technical writers comfortable with Markdown and Git-based review. Once non-technical editors need a UI, scheduled publishing, or asset management beyond what a file tree gives you, pairing a headless CMS with a Content Layer loader is usually less friction than building that tooling around raw Markdown files yourself.
| Use case | Fit |
|---|---|
| Developer-authored blog/docs | Local Markdown collection, schema-validated |
| Marketing site with non-technical editors | Content Layer loader + headless CMS |
| Data-driven pages (product catalog, etc.) | Content Layer loader against an API/database |
Wrapping up
Content collections turn what used to be an informal folder-of-Markdown convention into something the build actually checks. Start with local collections and a strict schema for anything developer-authored, and reach for the Content Layer API's loader system only once content genuinely needs to live outside the repository.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.