
Netsuki
Onii-chan~, this is the final episode of the Astro series♪
Today I’m gonna explain Content Collections(´∀`)

Onii-chan
Final episode. What’s it do?

Netsuki
It lets you handle Markdown files in a type-safe way!
Typos in frontmatter, missing required fields—it catches all that at build time♪
The Classic Frontmatter Tragedy

Netsuki
First, lemme show you a common problem with regular Markdown blogs.
---
title: Today's Diary
date: 2025-12-10
tags: [diary, chat]
---
Content here...

Netsuki
Looks fine, right?
But then…
---
titl: Today's Diary # ← Typo!
date: 'December 10' # ← Wrong format!
tag: [diary, chat] # ← tag instead of tags!
---

Netsuki
With regular Markdown, this builds just fine(´;ω;`)
Then in production: “Wait… why isn’t the title showing?”

Onii-chan
Debugging that sounds painful.

Netsuki
That’s where Content Collections saves the day!
Define a schema, and it yells at you during build time(≧∇≦)
Content Collections Basics

Netsuki
You set it up in src/content.config.ts♪
// 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 };

Netsuki
Yep yep! Astro uses Zod for validation♪
So you can say “title is a string”, “date is a date”, “tags is an array of strings”(´∀`)
What Happens When You Typo?

Netsuki
Try building with that typo-filled Markdown…
Error: blog → 2025-12-10.md frontmatter does not match collection schema.
"title" is required.
"date" must be a valid date.
"tags" is required.

Netsuki
It catches it right at build time!(゚∀゚)
You’ll know before deploying to production♪
Fetching Data from Collections

Netsuki
Use getCollection() to grab entries from your collection♪
---
// 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>

Netsuki
post.data.title, post.data.date—you get full autocomplete!(≧∇≦)
Hit . in VSCode and boom, suggestions pop up♪
Filtering Collections

Netsuki
You can filter with the second argument of getCollection()♪
---
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');
});
---

Netsuki
Draft functionality, tag filtering—super easy to implement(´∀`)
Getting a Single Entry

Netsuki
Need just one entry? Use getEntry()♪
---
// 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>

Onii-chan
So render() converts Markdown to HTML.

Netsuki
Right right! You can render it as a <Content /> component♪
How This Site Uses It

Netsuki
This site uses Content Collections too♪
Here’s what the diary schema looks like.
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(),
}),
});

Netsuki
Japanese and English versions are separate collections(´∀`)
So if I forget any frontmatter, I know right away♪

Onii-chan
Works for multilingual sites too.
Handy Zod Validations

Netsuki
Here’s a cheat sheet for common Zod patterns♪
| 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 |

Onii-chan
So coerce handles type conversion.

Netsuki
Yep! date: 2025-12-10 in frontmatter automatically becomes a Date object♪
Astro Series Wrap-up

Netsuki
Let’s look back at our 3-day Astro journey♪
| 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 |

Onii-chan
Covered basics to advanced stuff.

Netsuki
I know right?!(≧∇≦)
With these three, you can totally build a blog site♪
Components for shared parts
Dynamic routing for page generation
Content Collections for type-safe content
Pretty solid framework, huh?(´∀`)

Netsuki
If you’re curious, check out the official tutorial too♪
And that’s a wrap on Netsuki’s Astro intro series!
I’ll write about something fun again sometime(´∀`)

Netsuki
Ehehe~, thanks for reading♪(〃´∪`〃)