Type-Safe Markdown! Intro to Astro Content Collections♪
Netsuki
Type-Safe Markdown! Intro to Astro Content Collections♪
#Web
The Classic Frontmatter Tragedy
---
title: Today's Diary
date: 2025-12-10
tags: [diary, chat]
---
Content here...
---
titl: Today's Diary # ← Typo!
date: 'December 10' # ← Wrong format!
tag: [diary, chat] # ← tag instead of tags!
---
Content Collections Basics
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()),
description: z.string().optional(),
}),
});
export const collections = { blog };
What Happens When You Typo?
Error: blog → 2025-12-10.md frontmatter does not match collection schema.
"title" is required.
"date" must be a valid date.
"tags" is required.
Fetching Data from Collections
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
---
<h1>Blog Posts</h1>
<ul>
{
posts.map((post) => (
<li>
<a href={`/blog/${post.id}/`}>{post.data.title}</a>
<time>{post.data.date.toLocaleDateString('en-US')}</time>
</li>
))
}
</ul>
Filtering Collections
---
import { getCollection } from 'astro:content';
// Exclude drafts
const publishedPosts = await getCollection('blog', ({ data }) => {
return data.draft !== true;
});
// Only tech posts
const techPosts = await getCollection('blog', ({ data }) => {
return data.tags.includes('tech');
});
---
Getting a Single Entry
---
// src/pages/blog/[...slug].astro
import { getEntry } from 'astro:content';
const { slug } = Astro.params;
const post = await getEntry('blog', slug);
if (!post) {
return Astro.redirect('/404');
}
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<time>{post.data.date.toLocaleDateString('en-US')}</time>
<Content />
</article>
How This Site Uses It
const diary = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/diary' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()),
description: z.string(),
}),
});
const diaryEn = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/diary-en' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()),
description: z.string(),
}),
});
Handy Zod Validations
| Syntax | Meaning |
|---|---|
z.string() | String (required) |
z.string().optional() | String (optional) |
z.string().default('fallback') | With default value |
z.coerce.date() | Date (converts from string) |
z.array(z.string()) | Array of strings |
z.boolean() | Boolean |
z.enum(['a', 'b', 'c']) | Enum type |
Astro Series Wrap-up
| Date | Topic | What We Learned |
|---|---|---|
| 12/8 | Basics | Components, layouts, Islands |
| 12/9 | Dynamic Routing | [slug], getStaticPaths |
| 12/10 | Content Collections | Schema definitions, type-safe Markdown |
♪ Web Clap ♪
0 claps