Content-driven websites often store articles, documentation, tutorials, product descriptions, and other structured information in Markdown or MDX files. This approach makes content easier to maintain, but it also introduces a common problem: content can become inconsistent as a project grows.
An article might be missing its title, contain an incorrectly formatted publication date, or use a category that the website does not recognize. These mistakes may remain unnoticed until a page is generated or a deployment fails.
Astro Content Collections help solve this problem by organizing related content and validating its structure with schemas. Developers can define which fields are required, specify data types, and establish rules for values such as publication dates, categories, and tags.
This article explains how Astro Content Collections work, how to define schemas, how to query validated content, and how to handle validation errors in a production website.
What Are Astro Content Collections?
Astro Content Collections provide a structured way to organize and query content within an Astro project.
Instead of reading every Markdown file independently, developers can group related content into collections. For example, a technical blog might contain these collections:
- Blog articles
- Documentation pages
- Product announcements
- Author profiles
Each collection can have its own schema describing the expected structure of its entries.
Consider a blog article with the following frontmatter:
---
title: "Understanding TypeScript Generics"
description: "Learn how generics improve reusable TypeScript code."
pubDate: 2026-10-28
category: "TypeScript"
tags:
- TypeScript
- Web Development
draft: false
---
The frontmatter contains metadata that Astro can validate before the website uses the entry.
Without a schema, inconsistent fields can become difficult to manage. One article might use pubDate, while another uses publishedAt. A category might be a string in one file and an array in another.
A collection schema defines the expected structure and helps detect these problems early.
Create a Content Collection
Start with an Astro project that uses a compatible version of Astro and its Content Collections API.
A typical project structure might look like this:
src/
├── content/
│ └── blog/
│ ├── typescript-generics.md
│ └── astro-content-collections.md
├── content.config.ts
├── layouts/
│ └── BlogLayout.astro
└── pages/
└── blog/
└── index.astro
In current Astro projects, collection configuration is commonly defined in src/content.config.ts. Older Astro projects may use the legacy src/content/config.ts location and older collection APIs, so use the structure supported by your installed Astro version.
Each Markdown file in the blog directory represents a blog entry.
The configuration file defines the collection and its validation schema.
Define a Schema With Zod
Astro uses Zod-based schemas to validate structured content.
The following example defines a collection for blog articles:
// src/content.config.ts
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
category: z.string(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false)
})
});
export const collections = {
blog
};
Let's examine the important parts.
Define the Collection
const blog = defineCollection({
schema: z.object({
// Fields go here
})
});
The defineCollection() function creates a content collection configuration.
The schema describes the metadata expected in each entry. Astro uses it to validate content when the collection is processed.
Validate Required Strings
title: z.string(),
description: z.string(),
category: z.string()
These fields must contain strings.
If an article omits title or supplies a value with an incompatible type, validation fails.
Requiring descriptions is particularly useful for blogs because descriptions may be used in article listings, page metadata, and search previews.
Validate Publication Dates
pubDate: z.coerce.date()
This schema converts supported date inputs into JavaScript Date objects before validating them.
For example, a frontmatter value such as:
pubDate: 2026-10-28
can be converted to a date object.
Using z.coerce.date() is useful when content sources represent dates as strings or other supported input values. If the project requires stricter input formats, validate the original representation explicitly instead of relying only on coercion.
A publication date should be treated as content data, not as an arbitrary string. Validating it makes sorting and date formatting more predictable.
Provide Defaults for Optional Fields
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false)
The tags field must be an array of strings when supplied. If it is omitted, the schema provides an empty array.
Similarly, draft defaults to false when omitted.
Defaults are useful when a field has a sensible fallback value. They should not be used to conceal missing information that is essential to the content.
Validate Blog Articles
With the collection defined, create a Markdown file.
---
title: "Understanding TypeScript Generics"
description: "Learn how generics improve reusable TypeScript code."
pubDate: 2026-10-28
category: "TypeScript"
tags:
- TypeScript
- Web Development
draft: false
---
## Introduction
TypeScript generics help developers create reusable components,
functions, and data structures while preserving type information.
## Why Generics Matter
Generics allow a function to work with different types without
discarding useful type information.
Astro validates the frontmatter against the collection schema.
The article includes all required fields, uses the expected data types, and supplies an array of strings for tags.
Now consider an incorrect entry:
---
title: "Understanding TypeScript Generics"
description: "Learn how generics improve reusable TypeScript code."
pubDate: "not-a-date"
category: "TypeScript"
tags: "TypeScript"
draft: "false"
---
This entry contains several problems:
pubDate cannot be converted into a valid date.tags is a string rather than an array of strings.draft is a string rather than a boolean.
The schema helps Astro report these issues instead of allowing inconsistent metadata to spread throughout the application.
The exact error output depends on the Astro and Zod versions, but the underlying goal remains the same: identify invalid content before it causes problems in the generated website.
Query a Content Collection
After defining and validating the collection, retrieve its entries with Astro's content API.
For a blog index page, use getCollection():
---
// src/pages/blog/index.astro
import { getCollection } from "astro:content";
const posts = await getCollection("blog");
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Developer Blog</title>
</head>
<body>
<main>
<h1>Developer Blog</h1>
<ul>
{
posts.map((post) => (
<li>
<h2>{post.data.title}</h2>
<p>{post.data.description}</p>
<p>Category: {post.data.category}</p>
</li>
))
}
</ul>
</main>
</body>
</html>
The getCollection("blog") call retrieves entries from the blog collection.
Each entry separates its metadata from its content:
post.data contains validated frontmatter fields.post.body contains the raw Markdown body where available.post.id identifies the entry.- Other entry properties depend on the collection type and Astro version.
In the example, post.data.title and post.data.description are available as typed values based on the schema.
This makes content queries easier to maintain than accessing arbitrary frontmatter properties without validation.
Filter Draft Articles
Many websites store drafts alongside published articles. A schema can define a draft field, but the query must also use it to control which entries appear.
For example:
---
import { getCollection } from "astro:content";
const posts = await getCollection(
"blog",
({ data }) => data.draft === false
);
---
<ul>
{
posts.map((post) => (
<li>
<a href={`/blog/${post.id}/`}>
{post.data.title}
</a>
</li>
))
}
</ul>
This example includes only entries where draft is explicitly false.
Because the schema defaults draft to false, entries that omit the field receive that value after validation.
For production sites, confirm that the draft policy matches the deployment process. Some teams prefer to exclude drafts explicitly, while others maintain separate collections or build environments for unpublished content.
If draft content must never reach a public deployment, do not rely solely on hiding it in the user interface. Ensure the routing and build process exclude it from public output as required.
Sort Articles by Publication Date
A blog index commonly displays the newest articles first.
Because pubDate is validated as a date, entries can be sorted using the JavaScript date value.
---
import { getCollection } from "astro:content";
const posts = await getCollection(
"blog",
({ data }) => data.draft === false
);
posts.sort(
(a, b) =>
b.data.pubDate.getTime() - a.data.pubDate.getTime()
);
---
The subtraction compares the timestamps of the two dates.
The entry with the later publication date appears first. This approach is more reliable than sorting dates as arbitrary strings, especially when the content includes different date formats.
If the website supports scheduled publishing, filtering should also consider the current time and the project's intended publication rules.
Use Strict Schemas for Consistent Content
A basic schema validates data types, but production websites often need more precise rules.
For example, a blog may allow only a fixed set of categories.
Restrict Categories to Known Values
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
schema: z.object({
title: z.string().min(1),
description: z.string().min(20),
pubDate: z.coerce.date(),
category: z.enum([
"TypeScript",
"JavaScript",
"Web Development",
"Cloud"
]),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false)
})
});
export const collections = {
blog
};
This schema introduces two additional constraints.
First, title must contain at least one character. Second, description must contain at least 20 characters.
The category must also match one of the supported values.
These checks can prevent inconsistent category names such as WebDev, Web Development, and web-development from appearing in different entries when the site expects one standard value.
The description length is only an example. Choose a minimum that reflects your editorial requirements rather than assuming that every website needs the same limit.
Validate Tags
A tag array can be refined to reject empty strings:
tags: z.array(
z.string().trim().min(1)
).default([])
This validation trims whitespace and requires every tag to contain at least one character after trimming.
It helps prevent values such as an empty string or a whitespace-only tag from entering the collection.
For more advanced projects, you can also normalize tag casing or enforce a predefined set of tags. Keep the validation rules aligned with how tags are displayed, searched, and stored.
Organize Different Content Types
A website may contain both blog articles and documentation pages. These content types often require different metadata.
Astro allows each collection to define its own schema.
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
author: z.string(),
draft: z.boolean().default(false)
})
});
const documentation = defineCollection({
schema: z.object({
title: z.string(),
section: z.string(),
order: z.number().int().nonnegative()
})
});
export const collections = {
blog,
documentation
};
The blog collection requires publication information and an author. The documentation collection requires a section and an ordering value.
This separation avoids forcing every content type to use the same fields.
It also makes content queries easier to understand. A documentation page can be sorted by order, while blog articles can be sorted by pubDate.
When choosing between collections, consider whether the content types have meaningfully different validation requirements, publishing workflows, or routing rules.
Common Mistakes and Troubleshooting
| Problem | Possible cause | Recommended solution |
|---|
| Content validation fails | A required field is missing or has the wrong type | Compare the frontmatter with the collection schema |
| Publication dates cannot be parsed | The date value is invalid or uses an unexpected format | Use a consistent date format and validate the input |
| Tags cause a type error | A string was supplied instead of an array | Use a YAML list or the expected array representation |
| Drafts appear on the public website | The collection query does not filter them out | Apply the draft policy in the query and routing logic |
| Category filters return incomplete results | Content uses inconsistent category names | Use an enum or another explicit validation rule |
| Existing content breaks after a schema change | Old entries no longer satisfy the updated schema | Audit existing content before making the schema stricter |
| Sorting produces unexpected results | Dates are stored or processed inconsistently | Validate date values and sort using timestamps |
When a schema change causes validation failures, review the affected content before weakening the schema. A validation error may reveal a real content-quality problem rather than a problem with the validation system.
For larger repositories, include the Astro build or content validation process in continuous integration so that invalid content is detected before deployment.
Best Practices for Production Projects
Keep Schemas Aligned With Editorial Requirements
Do not add validation rules simply because they are technically possible. Each rule should reflect a real requirement, such as mandatory descriptions, valid categories, or consistent publication dates.
Overly restrictive schemas can slow down routine content updates.
Use Defaults Carefully
Defaults are useful for optional fields such as tags and draft status. Avoid assigning default values to essential fields when doing so would hide missing information.
For example, a missing article title should normally produce a validation error rather than silently receiving a generic title.
Validate Content During CI
Run the project's standard build or validation process before deploying changes. This helps catch invalid frontmatter before it reaches production.
For repositories with many contributors, automated validation also creates a consistent review process.
Plan Schema Changes
When introducing a required field or changing a field's type, inspect existing content first. Update the affected entries or design a migration strategy before enforcing the new schema.
This is particularly important for large documentation sites and blogs maintained by multiple authors.
Keep Content and Presentation Separate
Use schemas to validate content metadata and Astro components to render it.
This separation allows developers to improve the site's layout without changing the underlying content format unnecessarily.
Advantages and Limitations of Astro Content Collections
Advantages
- Type-safe content access: Schemas provide predictable types for content metadata.
- Early error detection: Invalid frontmatter can be identified during content processing.
- Consistent content structure: Required fields and allowed values reduce inconsistency.
- Easier querying: Collections provide a structured API for filtering and sorting entries.
- Better maintainability: Content rules are defined centrally instead of being repeated across pages.
Limitations
- Schema maintenance: Rules must evolve alongside the content model.
- Migration effort: Existing entries may need updates when validation becomes stricter.
- Limited scope: Frontmatter validation does not automatically validate every aspect of the Markdown body.
- Additional configuration: Small sites may need only a simple schema, while complex sites require more planning.
- Version differences: Projects using older Astro content APIs may need configuration changes when upgrading.
Conclusion
Astro Content Collections provide a structured way to manage Markdown and other supported content while validating metadata against clearly defined schemas. By combining collection definitions with Zod validation, developers can catch missing fields, incorrect data types, invalid dates, and inconsistent categories before those problems affect the website.
For production projects, begin with a schema that reflects the content's actual requirements. Add stricter validation where it improves consistency, filter drafts according to the publishing workflow, and run content validation as part of the deployment process.
The result is a content system that is easier to query, maintain, and extend as a website grows.
Astro Content CollectionsAstroZod schemascontent validationMarkdown frontmatterTypeScript
Join the conversation! Your thoughts help the community grow.