Content Collections
Defining a collection
Section titled “Defining a collection”Collections live in one config file: src/content.config.ts. Each collection is a call to defineCollection with two parts — a loader (where the content comes from) and a schema (its validated shape):
import { defineCollection } from 'astro:content';import { z } from 'astro/zod';import { glob } from 'astro/loaders';
const blog = defineCollection({ // loader: read every .md/.mdx file under src/data/blog (skipping _drafts) loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/data/blog' }), // schema: validate each file's frontmatter schema: z.object({ title: z.string(), pubDate: z.coerce.date(), draft: z.boolean().default(false), }),});
export const collections = { blog };The key Astro 6 rule: every collection must declare a loader. The old behavior — dropping files in src/content/blog/ and having them auto-collected — is gone. Being explicit means the same API can load local files today and a remote API tomorrow, with no change to how you query.
Loaders: where content comes from
Section titled “Loaders: where content comes from”Astro ships two built-in loaders from astro/loaders:
glob()— load many files matching a pattern. Each file becomes one entry. Use it for a folder of blog posts or docs pages:glob({ pattern: '**/*.md', base: './src/data/blog' }).file()— load many entries from a single data file (a JSON or YAML array). Use it for structured data like authors or products:file('./src/data/authors.json').
The pattern **/[^_]*.{md,mdx} is worth reading: it matches .md and .mdx files in any subfolder, but the [^_] excludes files starting with an underscore — a handy convention for drafts.
Beyond these, the loader is a documented interface, so the ecosystem provides loaders for CMSs and APIs; you can also write your own to pull from any source.
flowchart LR
src["files: src/data/blog/*.md"] --> loader["glob loader reads files"]
loader --> schema["schema validates frontmatter"]
schema --> store["typed entries in the content store"]
store --> get["getCollection('blog')"]
get --> page["page renders each entry"] Querying a collection
Section titled “Querying a collection”Once defined, you query a collection from any component script with two functions from astro:content:
---import { getCollection, getEntry } from 'astro:content';
// All published posts, newest firstconst posts = (await getCollection('blog')) .filter((post) => !post.data.draft) .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
// A single entry by its idconst featured = await getEntry('blog', 'hello-world');---<ul> {posts.map((post) => ( <li> <a href={`/blog/${post.id}`}>{post.data.title}</a> </li> ))}</ul>Each entry has an id (derived from its filename) and a data object — the validated frontmatter, fully typed from your schema. Because validation already ran, post.data.pubDate is a real Date, not a string; your editor autocompletes the fields.
Rendering an entry’s body
Section titled “Rendering an entry’s body”Querying gives you an entry’s frontmatter, but the Markdown body isn’t HTML yet. To render it, call render (from astro:content) on the entry — it returns a <Content /> component you drop into the template:
---import { getEntry, render } from 'astro:content';
const post = await getEntry('blog', 'hello-world');if (!post) throw new Error('Post not found');
const { Content } = await render(post);---<article> <h1>{post.data.title}</h1> <time>{post.data.pubDate.toDateString()}</time> <Content /></article>render compiles the entry’s Markdown/MDX to a component, so <Content /> outputs the finished HTML — headings, code blocks, and any components used in MDX all rendered.