- Author
- app-hacks
- Published
- Updated
- Reviewed
Astro / TypeScript / Content Collections
Astro Content Collections: Building a Robust, Type-Safe Blog with Zod
Learn how to validate Markdown frontmatter with Astro 7 Content Collections, keep dates and tags consistent, and retrieve article lists with reliable inferred types.
Simply loading Markdown files does not catch missing frontmatter or inconsistent value types early. If one article treats its publication date as a string and another as a Date, sorting and formatting soon accumulate defensive branches. Content Collections place the loader and metadata schema at one boundary so invalid content fails before it reaches a page.
Versions and verification scope
This article was checked on September 11, 2026 against Astro 7.3.2 and Node.js 22.22.2 installed in this repository. The actual src/content.config.ts loads Markdown under src/content/blog with glob() and validates frontmatter with Zod. External CMS integrations and MDX are outside this verification scope.
Define the collection
Astro 7 build-time collections are defined in src/content.config.ts. For local Markdown files, use the glob() loader from astro/loaders and z from astro/zod.
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.md' }),
schema: z.object({
title: z.string().min(1),
description: z.string().min(1),
pubDate: z.coerce.date(),
author: z.string().min(1).optional(),
updatedDate: z.coerce.date().optional(),
reviewedAt: z.coerce.date().optional(),
tags: z.array(z.string().min(1)).min(1),
}),
});
export const collections = { blog };
base identifies the content directory and pattern selects files. Use **/*.md while Markdown is the only supported format; expand it only if the project actually adopts MDX. z.string().min(1) rejects empty titles and descriptions, and the tag schema requires at least one non-empty item. These constraints should come from display requirements rather than arbitrary strictness.
z.coerce.date() makes the final collection value a Date, regardless of how the frontmatter parser initially represents it. Page code can then sort and format dates without repeating conversions.
Match frontmatter to the schema
---
title: "Start Type-Safe Article Management with Astro"
description: "A minimal Content Collections setup."
pubDate: 2026-09-01
author: "app-hacks"
updatedDate: 2026-09-11
reviewedAt: 2026-09-11
tags: ["Astro", "TypeScript"]
---
Field names and shapes must match exactly. Renaming pubDate to publishedAt, or writing tags as a comma-separated string, violates the contract. Optional fields should be declared explicitly so adding metadata does not break every existing article.
Retrieve a typed article list
getCollection() returns entries whose data type is inferred from the schema.
---
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.id}/`}>{post.data.title}</a>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString('en-US')}
</time>
</li>
))}
</ul>
Because pubDate is known to be a Date, valueOf() and toISOString() are safe. Filtering can also be passed to getCollection(), but page code must only rely on fields that really exist in the schema.
Generate detail pages statically
---
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({ params: { slug: post.id }, props: { post } }));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<article><h1>{post.data.title}</h1><Content /></article>
Adding a Markdown file now triggers loader discovery, schema validation, and static route generation in one build. Using the file-derived id avoids maintaining a duplicate slug field.
Include validation in publishing
Run nvm use and then npm run build after adding or changing an article. The build checks Markdown loading, schema validation, template types, and static page generation. Optional author and review fields preserve compatibility; making them required will correctly fail articles that lack values. Keep the boundary clear: Markdown supplies content, the schema validates input, and pages consume only validated data.