Overview
Content collections give a folder or data file a Zod schema, validate every entry at build time, and return typed objects from queries. Use them for any set of Markdown, MDX, JSON, YAML, or TOML entries that share a shape; use plain src/pages/ files for one-off pages. The Content Layer API (loaders) arrived in Astro 5, and Astro 6 removed the legacy type: "content" and type: "data" form entirely.
Define collections with a loader in src/content.config.ts
Each collection takes a loader and a schema. Import z from astro/zod, which re-exports the Zod 4 version Astro uses; z from astro:content and astro:schema are deprecated since Astro 6.
// src/content.config.ts
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { z } from "astro/zod";
const posts = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/posts" }),
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { posts };Use z.coerce.date() for dates, because YAML dates arrive as strings. Zod 4 moved string formats to the top level (z.email(), not z.string().email()), and a .default() combined with a transform must match the output type. Restart the dev server or press s + enter after a schema change so astro:content regenerates.
Query with getCollection, getEntry, and render
Filter drafts and sort in the query, not the template. Entries have no render() method; import render from astro:content.
import { getCollection, getEntry, render } from "astro:content";
const posts = (await getCollection("posts", ({ data }) => !data.draft))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
const post = await getEntry("posts", "my-post"); // undefined if missing
const { Content, headings } = await render(post!);Entries are identified by id, generated from the filename in a URL-friendly, lowercased form. Override one entry’s id with a slug field in its frontmatter, or change the rule for the collection with the loader’s generateId. Build static routes from post.id in getStaticPaths(), and use a rest parameter ([...id]) when ids contain slashes.
Use the file() loader for data files
Use file("./src/content/authors.json") for one JSON, YAML, or TOML file holding many entries, such as authors, navigation, or SKUs. Every entry needs a unique id. Data entries have no body and nothing to render(). Use glob() with a data pattern when each entry is its own file.
Relate collections with reference()
Import reference from astro:content and declare author: reference("authors"). Astro validates that the target exists, and getEntry(post.data.author) returns the typed author. Reference instead of copying author data into every post.
Use live collections for request-time data
A live collection fetches from a CMS or API on each request instead of at build time. Define it in src/live.config.ts with defineLiveCollection and query it with getLiveCollection and getLiveEntry, which return { entries | entry, error } and an optional cacheHint. It requires on-demand rendering (export const prerender = false) and has no MDX support or image optimization. Pass cacheHint to Astro.cache.set() to apply the loader’s caching strategy (route caching is stable in Astro 7). Live collections are stable as of Astro 6.
Defer rendering for very large Markdown collections
If a large .md collection runs the build out of memory, set deferRender: true (Astro 7.1) on the glob() loader. Rendering then happens when each entry is rendered in a page, at the cost of not caching rendered HTML in the data store. It does not apply to MDX, Markdoc, or data entries.
Verify at build time
astro build fails on any entry that violates its schema, and astro check surfaces the same errors, plus .astro type errors, before a build. This validation on every build is the main advantage over raw import.meta.glob.