Content Collections
Overview
Section titled “Overview”Astro v5 introduced the Content Layer API — a performant, scalable system for defining and querying content. Collections provide:
- Type safety via Zod schemas
- Intellisense in your editor
- Built-in loaders for local files (
glob(),file()) - Custom loaders for remote data (CMS, database, API)
Configuration File
Section titled “Configuration File”Define collections in src/content.config.ts:
// 1. Import utilitiesimport { defineCollection, reference } from 'astro:content';
// 2. Import loadersimport { glob, file } from 'astro/loaders';
// 3. Import Zodimport { z } from 'astro/zod';
// 4. Define collectionsconst blog = defineCollection({ loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }), schema: z.object({ title: z.string(), description: z.string(), pubDate: z.coerce.date(), draft: z.boolean().default(false), tags: z.array(z.string()).default([]), })});
// 5. Exportexport const collections = { blog };Built-in Loaders
Section titled “Built-in Loaders”glob() — One File Per Entry
Section titled “glob() — One File Per Entry”For directories of Markdown, MDX, JSON, YAML, or TOML files:
const posts = defineCollection({ loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }), schema: z.object({ /* ... */ })});- Each file becomes one entry
idis auto-generated from the filename- Supports glob patterns:
['*.md', '!draft-*']
file() — Multiple Entries From One File
Section titled “file() — Multiple Entries From One File”For a single JSON/YAML/TOML file containing multiple entries:
const dogs = defineCollection({ loader: file("src/data/dogs.json"), schema: z.object({ id: z.string(), breed: z.string(), temperament: z.array(z.string()), })});With a custom parser (e.g., CSV):
import { parse as parseCsv } from "csv-parse/sync";
const products = defineCollection({ loader: file("src/data/products.csv", { parser: (text) => parseCsv(text, { columns: true, skipEmptyLines: true }) })});Remote/Custom Loaders
Section titled “Remote/Custom Loaders”Inline Loader (Simple)
Section titled “Inline Loader (Simple)”const countries = defineCollection({ loader: async () => { const res = await fetch("https://restcountries.com/v3.1/all"); const data = await res.json(); // Must return array with `id` property return data.map((c) => ({ id: c.cca3, ...c })); }, schema: z.object({ name: z.object({ common: z.string() }), population: z.number(), })});Loader Object (Advanced)
Section titled “Loader Object (Advanced)”For incremental updates, caching, or publishing as npm packages — see the Content Loader API reference.
Schema Patterns
Section titled “Schema Patterns”Common Field Types
Section titled “Common Field Types”schema: z.object({ // Strings title: z.string(), slug: z.string().optional(),
// Dates (coerce parses date strings) pubDate: z.coerce.date(), updatedDate: z.coerce.date().optional(),
// Booleans with defaults draft: z.boolean().default(false), featured: z.boolean().default(false),
// Arrays tags: z.array(z.string()).default([]),
// Enums category: z.enum(['tutorial', 'guide', 'reference']),
// Numbers readingTime: z.number().optional(),
// URLs heroImage: z.string().url().optional(), externalLink: z.string().url().optional(),})Collection References
Section titled “Collection References”Link entries across collections:
const blog = defineCollection({ loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }), schema: z.object({ title: z.string(), author: reference('authors'), // Single reference relatedPosts: z.array(reference('blog')), // Array of references })});
const authors = defineCollection({ loader: glob({ pattern: "**/*.json", base: "./src/content/authors" }), schema: z.object({ name: z.string(), email: z.string().email(), })});Querying
Section titled “Querying”Get All Entries
Section titled “Get All Entries”---import { getCollection } from 'astro:content';
const posts = (await getCollection('blog')) .filter(post => !post.data.draft) .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());---Get Single Entry
Section titled “Get Single Entry”---import { getEntry } from 'astro:content';
const post = await getEntry('blog', 'my-first-post');---Filter by Subdirectory
Section titled “Filter by Subdirectory”---const englishDocs = await getCollection('docs', ({ id }) => { return id.startsWith('en/');});---Resolve References
Section titled “Resolve References”---import { getEntry, getEntries } from 'astro:content';
const post = await getEntry('blog', 'welcome');const author = await getEntry(post.data.author);const related = await getEntries(post.data.relatedPosts);---Rendering Content
Section titled “Rendering Content”---import { getEntry, render } from 'astro:content';
const post = await getEntry('blog', 'my-post');const { Content, headings } = await render(post);---
<article> <h1>{post.data.title}</h1> <Content /> <!-- Rendered markdown/MDX --></article>Dynamic Routes
Section titled “Dynamic Routes”Static (Default)
Section titled “Static (Default)”---import { getCollection, render } from 'astro:content';
export async function getStaticPaths() { const posts = await getCollection('blog'); return posts.map(post => ({ params: { slug: post.id }, // v5 uses post.id, not post.slug props: { post }, }));}
const { post } = Astro.props;const { Content } = await render(post);---
<Content />SSR (On-Demand)
Section titled “SSR (On-Demand)”---import { getEntry, render } from 'astro:content';
const { id } = Astro.params;if (!id) return Astro.redirect('/404');
const post = await getEntry('blog', id);if (!post) return Astro.redirect('/404');
const { Content } = await render(post);---
<Content />