Development & Building with AI · Explainer

Astro Content Collections: content at scale

When a site grows from a few pages to hundreds of articles, the problem is no longer writing but keeping everything consistent. See how Astro solves this with typed collections.

Responsible editor:
Daniel Ferreira

Published · 6 min read

Originally published in Brazilian Portuguese.

Translated from the Portuguese original with AI assistance.

Also available in: Português

Key points

  • A collection groups entries with the same structure; a loader says where the data comes from and a Zod schema says what is valid.
  • An entry that breaks the schema makes the build fail with an explanatory error, which turns editorial rules into automatic checks.
  • References between collections link articles to authors and sources, but the code still needs to handle a reference that is not found.

A site with ten articles fits in the head of whoever maintains it. With two hundred, in several languages, it no longer does. Someone forgets the publication date, types the category with an accent, points to an author who does not exist. None of this produces a visible error: the page simply comes out wrong.

The Content Collections of Astro, the site framework that generates pages from components and content files, exist for this problem. This article explains how they work according to the official documentation and shows, as a documented case, how they are used on the Token & Tech site itself.

What a collection is

The Astro documentation defines a collection as a set of data with the same structure, such as blog posts or product cards. Each item is an entry.

The documentation lists three situations in which collections are worth using:

  • multiple entries with the same structure, such as a folder of Markdown files with the same frontmatter;
  • remote content, from a CMS (content management system), that you want to query with the same functions;
  • thousands of entries processed at build time, the step that generates the site.

And it says when not to use them: single pages (which go in src/pages/), files that do not need processing, such as PDFs (which go in public/), or data sources whose own library you prefer to call directly.

Two modes: build time and live

There are two types of collection, according to the same page.

Build-time collections load data while the site is being generated and store it in a data layer. They suit relatively stable content, such as articles. They are defined in src/content.config.ts.

Live collections fetch data on every request. They suit data that changes all the time. They live in src/live.config.ts, require an on-demand rendering adapter and, according to the documentation, do not support MDX or image optimization.

For an editorial site published as static files, the natural path is the build-time collection. That is what the rest of this article covers.

The three pieces: collection, loader and schema

A collection is declared with defineCollection() and has two parts: a loader, which is required, and a schema, which is optional. All collections are exported together in a collections object.

The loader says where the data comes from. Astro ships with two, described in the loader API reference:

  • glob() reads multiple local files that match a pattern, such as **/*.md, from a base folder. It accepts Markdown, MDX, Markdoc, JSON, YAML and TOML.
  • file() reads multiple entries from a single JSON, YAML or TOML file. Each entry needs a unique id.

For other sources, such as an API, you write your own loader. The API provides, among other things, a per-collection store, a function that validates each entry before saving it and a digest, a fingerprint of the content that avoids rewriting what has not changed.

The schema says what a valid entry is. It is written with Zod, a validation library for TypeScript, imported from astro/zod. The schema plays two roles: it validates the data and generates the types the code editor uses for autocompletion. The documentation sums up the consequence: if a file violates the collection schema, Astro shows an error to let you know.

This loader layer debuted in Astro 5, released in December 2024. In the announcement, the company said collections now generated Markdown pages up to 5 times faster on content-heavy sites and reduced memory usage by 25% to 50% (Astro 5.0). These are figures released by the company itself, not independent measurements.

Identifiers, references and queries

ID. Each entry has an identifier. With glob(), it is generated from the file name. There is a detail that often surprises people: if the file has a slug field in its frontmatter, that field becomes the ID. The generateId option lets you define another rule.

References. The reference("authors") function declares, in the schema, that a field points to an entry in another collection. In the file, you write only the author’s ID. The astro:content module reference makes an important caveat: validation of the referenced entry happens when you fetch it with getEntry() or getEntries(). If it does not exist, the result is undefined, and your code needs to handle that case.

Queries. getCollection() returns all entries and accepts a filter, for example to keep only published ones. getEntry() returns one entry by ID. The documentation warns that the order of the result is not guaranteed: sort it yourself, by date or title.

Rendering. render(entry) returns a Content component with the body text and the list of headings, useful for building a table of contents.

Types. The astro sync command generates the types for the astro:content module (CLI reference). Astro also generates JSON Schema files in .astro/collections/, which editors like VS Code can use to validate YAML and JSON files as you type.

Editorial rules that become build errors

The schema does not have to stop at “this field is text”. With Zod, it can express rules that involve several fields. The .superRefine() method lets you record several problems in a single validation function (Zod 4 documentation).

This is where collections stop being just folder organization. A rule like “a published article needs a date and someone responsible for approval” stops being a checklist item and starts preventing the site from being generated. In the editorial team’s assessment, this is the main gain at scale: the rule is applied the same way to article 1 and article 200, including when the writer is an AI agent (see how Claude Code works).

The limit is also clear. The schema checks the shape of the data, not its truth. It guarantees that a source URL exists, not that the source says what the text claims.

Documented case: the Token & Tech site

The site you are reading is built with Astro, version 7 as of October 2026, and organizes its content in collections. The design is described in the project’s CONTENT_MODEL.md and implemented in src/content.config.ts. The excerpts below are abridged.

Article and translation are separate collections. Each article has a language-independent record (articles/<id>.yaml) with type, section, credits, status, approval and sources. Each language version is its own file (article-translations/<id>/<language>.md), with title, slug and text. The article ID is permanent and the same in every language; the slug changes per language.

Because translations have a slug field, the default behavior of glob() would use the slug as the ID. That is why the project defines the ID by the file path:

loader: glob({
  pattern: "[a-z0-9]*/[a-z]*.md",
  base: "./src/content/article-translations",
  generateId: ({ entry }) => entry.replace(/\.md$/, ""),
}),

The pattern serves another purpose too: files and folders starting with _ are templates and are never loaded.

Closed values come from typed data. Categories and topics are lists in TypeScript, and the schema accepts only those values, with z.enum(TOPIC_IDS). A mistyped topic does not create an orphan page: it breaks the build.

Relationships use reference(). Credits point to the authors collection, and sources point to the sources collection, in which each reference has a title, publisher, URL and access date.

Publication rules live in the schema. An excerpt from the articles’ superRefine:

if (a.status === "published" || a.status === "update-required") {
  if (!a.publishedAt) ctx.addIssue({ code: "custom", message: "published article needs publishedAt" });
  if (!a.approvedBy || !a.approvedAt || !a.approval) ctx.addIssue({ code: "custom", message: "..." });
}

There are similar rules for financial content (it requires additional review or a notice that review is in progress) and for news (which requires the date of the event). When building the pages, the code uses getCollection() to filter only articles with a publishable status and published translations.

The schema does not cover everything. It does not know whether an internal link points to a page that exists, or whether the versions in each language point to one another. For that, the project uses a checker that runs after the build, described in how to test internal links and routes automatically.

Takeaways

  • Collections make sense when there are many entries with the same structure. For a single page, a file in src/pages/ is enough.
  • Separate what does not change between languages (the article record) from what does (title, slug, text). This avoids duplicating metadata in every translation.
  • Use the schema for editorial rules, not just for types. A rule that breaks the build is always followed.
  • Remember that shape validation does not replace content checking, or testing of the generated site.

Disclosure: The Token & Tech site is built with Astro and uses the techniques described here.

Sources

  1. Content collections, Astro Docs. Accessed October 8, 2026
  2. Astro Content Loader API, Astro Docs. Accessed October 8, 2026
  3. Content Collections API Reference (astro:content), Astro Docs. Accessed October 8, 2026
  4. Astro 5.0, Astro. Published December 3, 2024 · Accessed October 8, 2026
  5. CLI Commands, Astro Docs. Accessed October 8, 2026
  6. Zod 4 API: refinements (.superRefine), Zod. Accessed October 8, 2026

Related reading

Development & Building with AI · Explainer

Astro: why use it for sites with thousands of pages

When a site has thousands of content pages, every architectural decision is multiplied. Based on the official documentation and dated field data, see what Astro does well in this scenario and where its limits are.

Development & Building with AI · Analysis

Astro vs. Next.js: architecture and performance compared

Astro and Next.js both generate HTML on the server, but they start from different premises: one was born for content sites, the other for React applications. With official documentation and dated field data, see what that changes in practice.

Development & Building with AI · Tutorial

How to automate technical SEO audits

A technical audit done once a quarter is out of date the next day. See how to split the checks into layers that run on their own with every release and fail before the problem reaches the search engine.