Skip to content

Content Collections

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):

src/content.config.ts
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.

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"]
The content collection pipeline

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 first
const 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 id
const 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.

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.

Where do you define content collections in Astro 6?
What is the difference between the glob and file loaders?
How do you render a collection entry's Markdown body to HTML?
After a schema validates, what type is a z.coerce.date() frontmatter field on entry.data?