Content Collections
By Syncfusion®, Inc. · Last updated
What collections are, src/content.config.ts, defineCollection(), the glob() and file() loaders, entry IDs, Zod schemas with optional fields and defaults, getCollection() with filters and sorting, getEntry(), render() and the Content component, and JSON Schema support.
Content Collections
- A blog with 50 posts has 50 frontmatter blocks. One typo, such as
titel:instead oftitle:, can break a page. - A content collection is a named group of similar content, such as
blogorauthors. - You tell Astro where the files are and what fields each one must have.
- Astro checks every file at build time, and gives you a simple way to read them.
- In this lesson, Asha turns her posts into a checked
blogcollection.
Simple lesson: a collection is a group of content that Astro loads, checks and serves to your pages.
A Real-World Example
- Ravi writes a post and types
pubDate: next week. - With plain Markdown files, the page builds, and shows "Invalid Date" to readers.
- With a collection, the build stops and says which file and which field is wrong.
- Asha fixes it before any reader sees it.
Press ▶ Run. The home page reads the collection. Then open src/content.config.ts.
What happened:
content.config.tsdefined a collection calledblog.glob()found the two.mdfiles.- The schema turned
pubDateinto a realDate. The second post gottags: []by default. - The Terminal shows how many entries were loaded.
What You Will Learn
In this lesson, you will learn:
- What collections are, and when to use one.
- How to define a collection in
src/content.config.ts. - The
glob()andfile()loaders. - How entry IDs are made.
- How to write a Zod schema with optional fields and defaults.
- How to get, filter and sort entries.
- How to render an entry's Markdown with
render().
What Are Content Collections?
Types of Collections
- Build-time collection = loaded once, when the site is built. This lesson covers these.
- Live collection = loaded on every request, for data that changes all the time. You meet them in Fresh Data at Scale.
When to Use a Collection
Use one when:
- you have many items of the same shape: posts, products, authors, docs pages,
- you want each item checked before the site is built,
- the content lives in files (Markdown, JSON, YAML) or comes from an API.
You do not need one for:
- one-off pages, such as the home or about page,
- small data used in one place.
A good sign you need a collection: you are copying the same frontmatter fields into a third file.
Collection Entries
- Entry = one item in a collection, such as one blog post.
Each entry has:
id: its unique name,data: the checked frontmatter or JSON fields,body: the raw Markdown text, for Markdown entries,collection: the collection's name.
Animation: Markdown files go through the loader, then the schema check. Good files become entries. A bad file stops at the schema with a red cross.
why-astro.md tips.md typo.md
glob()
Zod schema checks every field
✓ entry: why-astro ✓ entry: tips ✗ typo.md: pubDate is not a date. Build stops.
Every file goes through the loader and the schema. A bad file stops the build.
Simple lesson: use a collection for many items of the same shape. Each item becomes an entry with id and data.
Defining Collections
src/content.config.ts
- All collections are defined in one file:
src/content.config.ts(or.js). - It must export an object called
collections.
defineCollection()
defineCollection({ loader, schema })describes one collection.loader= where the entries come from.schema= the shape every entry must have. It is optional, but you should always add one.
The glob() Loader
glob()loads entries from files: Markdown, MDX, JSON or YAML.base= the folder to search.pattern= which files to take.glob({ pattern: "**/*.md", base: "./src/content/blog" })takes every.mdfile in that folder and its sub-folders.
The file() Loader
file()loads many entries from one file, such as a JSON list.- Each object in the list needs an
idfield.
Entry IDs and Custom IDs
With
glob(), the ID is made from the file path, in lower case, with no extension.why-astro.md→why-astro2026/My Post.md→2026/my-post
- Set your own ID with a
slugfield in the frontmatter. - With
file(), the ID is each object'sidfield.
Press ▶ Run. authors comes from one JSON file. blog comes from many Markdown files.
- The file
2026/Year In Review.mdgot the ID2026/year-in-review.
Simple lesson: glob() for many files, file() for one file with many items. IDs come from file names or id fields.
Schemas
Zod Schemas
- Schema = a description of the fields an entry must have, and their types.
- Astro uses Zod, a popular checking library. Import it with
import { z } from "astro/zod";. z.object({ ... })lists the fields.
Common Field Types
z.string()= text.z.number()= a number.z.boolean()=trueorfalse.z.coerce.date()= a date. "Coerce" means "turn the text into aDateif possible".z.array(z.string())= a list of text.z.enum(["draft", "published"])= one of a fixed set of values.
Optional Fields and Default Values
.optional()= the field may be missing. Its value is thenundefined..default(value)= the field may be missing. Its value is thenvalue.- Example:
draft: z.boolean().default(false).
[TS] Inferred Types from Schemas
- Astro turns your schema into TypeScript types.
- In your editor,
post.data.pubDateis known to be aDate, andpost.data.tagsastring[]. - Spelling mistakes, such as
post.data.titel, are underlined before you build. - [JS] You still get the check at build time. Your editor may also suggest field names.
Press ▶ Run. One post has a mistake, so the build stops. Read the Terminal, then fix the post.
- The error names the entry (
broken) and each wrong field. - Fix
titeltotitle, andreadytopublished. Then run again.
Use z.coerce.date() for dates. YAML may read 2026-11-10 as text, and coerce turns it into a real Date you can sort and format.
Simple lesson: a Zod schema lists the fields. A wrong or missing field stops the build with a clear message.
Querying Collections
getCollection()
await getCollection("blog")returns every entry as a list.- It runs at build time, in the component script.
Filtering Entries
- Pass a function as the second argument. Keep the entries where it returns
true. getCollection("blog", ({ data }) => !data.draft)leaves out drafts.
Sorting Entries
getCollection()does not promise any order. Sort the list yourself.- Newest first:
posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()).
getEntry() and getEntries()
await getEntry("blog", "why-astro")returns one entry by its ID, orundefined.await getEntries([...])returns several entries from a list of references. You use it in Pages from Content.
Press ▶ Run. Drafts are left out, and the newest post comes first.
Simple lesson: getCollection() for lists, with a filter and your own sort. getEntry() for one entry.
Rendering Entries
render() and the Content Component
entry.datahas the frontmatter. The Markdown body is not HTML yet.const { Content, headings } = await render(entry);turns it into HTML.- Use
<Content />in the template where the post's text should appear.
Rendering Headings
headingsis the same list as in a Markdown layout:depth,slugandtextfor each heading.- Use it to build a table of contents.
[TS] Typing Entries with CollectionEntry
CollectionEntry<"blog">is the type of one blog entry.- Use it for a component's props:
interface Props { post: CollectionEntry<"blog"> }.
import type { CollectionEntry } from "astro:content";
interface Props {
post: CollectionEntry<"blog">;
}
Editor Support
JSON Schema Files for Data Entries
- For JSON and YAML entries, Astro writes a JSON Schema file for each collection into the
.astro/collections/folder. - Point your JSON file to it with a
"$schema"field. VS Code then checks the file and suggests field names as you type.
In this lab, collections are loaded by a small version of Astro's content layer that runs in your browser. It uses the real Zod and Astro's real Markdown package. On your computer, Astro also caches entries between builds in the .astro folder.
Simple lesson: render() turns an entry's Markdown into a <Content /> component and a list of headings.
TL;DR
- Collection = a named group of similar content, checked at build time.
- src/content.config.ts =
export const collections = { blog, ... }. - defineCollection() = one collection:
defineCollection({ loader, schema }). Loaders:
glob({ pattern, base })= many files,file("path.json")= many items in one file.
- Entry =
id,data,body,collection. - Schema =
z.object({...})fromastro/zod. Use.optional(),.default(),z.coerce.date(). - getCollection(name, filter) = all entries. Sort them yourself.
- getEntry(name, id) = one entry.
- render(entry) =
{ Content, headings }.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
Not exporting collections | Defining is enough | export const collections = { blog } | getCollection("blog") finds nothing |
Wrong base folder | The path is relative to the config | Start from the project root: ./src/content/blog | Zero entries loaded |
z.date() for a text date | YAML dates are always dates | Use z.coerce.date() | The schema rejects the date |
| Expecting sorted results | getCollection() sorts by date | Sort the list yourself | Posts show in a random order |
Using post.body as HTML | The body is ready HTML | Use render() and <Content /> | Raw Markdown shows on the page |
| No schema | Schemas are only for TypeScript | Always add one | Typos reach the live site |
Check Yourself
Exercises
What to Remember
- A collection groups content of the same shape. Define it in
src/content.config.ts. glob()reads many files,file()reads one file with many items.- A Zod schema checks every entry and stops the build on mistakes.
getCollection()with a filter and a sort,getEntry()for one item.render()gives you<Content />andheadings.
Next: Pages from Content, where every post gets its own page, with pagination and tags.