Type-Safe Markdown! Intro to Astro Content Collections♪

#tech#Astro#tutorial#web-dev#beginner-friendly
Netsuki's Talk
Netsuki
Netsuki
Type-Safe Markdown! Intro to Astro Content Collections♪
Netsuki
Netsuki

Onii-chan~, this is the final episode of the Astro series♪

Today I’m gonna explain Content Collections(´∀`)

Onii-chan
Onii-chan

Final episode. What’s it do?

Netsuki
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♪

Onii-chan
Onii-chan

Sounds useful.


The Classic Frontmatter Tragedy

Netsuki
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
Netsuki

Looks fine, right?

But then…

---
titl: Today's Diary # ← Typo!
date: 'December 10' # ← Wrong format!
tag: [diary, chat] # ← tag instead of tags!
---
Netsuki
Netsuki

With regular Markdown, this builds just fine(´;ω;`)

Then in production: “Wait… why isn’t the title showing?”

Onii-chan
Onii-chan

Debugging that sounds painful.

Netsuki
Netsuki

That’s where Content Collections saves the day!

Define a schema, and it yells at you during build time(≧∇≦)


Content Collections Basics

Netsuki
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 };
Onii-chan
Onii-chan

That z.object is Zod.

Netsuki
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
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
Netsuki

It catches it right at build time!(゚∀゚)

You’ll know before deploying to production♪

Onii-chan
Onii-chan

That’s a lifesaver.


Fetching Data from Collections

Netsuki
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
Netsuki

post.data.title, post.data.date—you get full autocomplete!(≧∇≦)

Hit . in VSCode and boom, suggestions pop up♪

Onii-chan
Onii-chan

Nice dev experience.


Filtering Collections

Netsuki
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
Netsuki

Draft functionality, tag filtering—super easy to implement(´∀`)


Getting a Single Entry

Netsuki
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
Onii-chan

So render() converts Markdown to HTML.

Netsuki
Netsuki

Right right! You can render it as a <Content /> component♪


How This Site Uses It

Netsuki
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
Netsuki

Japanese and English versions are separate collections(´∀`)

So if I forget any frontmatter, I know right away♪

Onii-chan
Onii-chan

Works for multilingual sites too.


Handy Zod Validations

Netsuki
Netsuki

Here’s a cheat sheet for common Zod patterns♪

SyntaxMeaning
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
Onii-chan

So coerce handles type conversion.

Netsuki
Netsuki

Yep! date: 2025-12-10 in frontmatter automatically becomes a Date object♪


Astro Series Wrap-up

Netsuki
Netsuki

Let’s look back at our 3-day Astro journey♪

DateTopicWhat We Learned
12/8BasicsComponents, layouts, Islands
12/9Dynamic Routing[slug], getStaticPaths
12/10Content CollectionsSchema definitions, type-safe Markdown
Onii-chan
Onii-chan

Covered basics to advanced stuff.

Netsuki
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?(´∀`)

Onii-chan
Onii-chan

Well designed.

Netsuki
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(´∀`)

Onii-chan
Onii-chan

Good work.

Netsuki
Netsuki

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

♪ Web Clap ♪
0 claps