astro content collections

[check: …]
📖 Full guide → 📖 ASTRO_CONTENT_COLLECTIONS.md — the complete narrative, code samples, and verified sources. This page is its interactive companion.

The one idea — a typed gateway to your content

A content collection is a typed gateway to your content: define a Zod schema, point a loader at your files (or remote data), get fully-typed queries via getCollection() — no more any frontmatter. Astro 5's Content Layer unifies local + remote behind the same API.

1 · the config — src/content.config.ts (Astro 5 moved it here from src/content/config.ts)

Define each collection with defineCollection(), give it a required loader and an optional Zod schema, then export a single collections object.

// src/content.config.ts ← lives at the src/ root in Astro 5 (was src/content/config.ts in v4) import { defineCollection } from 'astro:content'; import { glob } from 'astro/loaders'; // glob() & file() added in astro@5.0.0 import { z } from 'astro/zod'; // Zod, re-exported by Astro const blog = defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/blog' }), schema: z.object({ title: z.string(), pubDate: z.coerce.date(), // '2025-01-02' → Date tags: z.array(z.string()), draft: z.boolean().optional(), // optional field }), }); export const collections = { blog };

2 · the content — src/content/blog/*.md

The glob() loader ingests each .md as an entry: an auto-generated id (kebab-cased from the filename), the parsed data (your frontmatter), and the raw body. Click an entry to load it into the validator below.

3 · schema validation — a live Zod-style safeParse()

A schema mismatch is a build error, not a runtime one — Astro refuses to ship an entry whose frontmatter fails safeParse(). Toggle a field below to break/fix the selected entry and watch the simulated parse result flip. (This is a faithful inline simulation of Zod's required/string/array/boolean rules.)

Mutate the selected entry's frontmatter:
pick an entry above, then toggle a field.

4 · the data flow — pick a loader

The Content Layer unifies local and remote: swap the loader and getCollection('blog') keeps the exact same typed shape. You are never limited to .md/.mdx.

What changed Astro 4 → 5: config moved src/content/config.tssrc/content.config.ts · type:'content'+dir → a required loader · entry.slugentry.id · entry.render() → standalone render(entry) · content can live anywhere (no longer pinned to src/content/).
Cross-refs (🔗): astro_routing_layouts (collections feed dynamic routes via getStaticPaths) · astro_islands (rendered MDX entry as a hydrated island).