[Astro] Content Collections + glob loader で Markdown ブログを作る

Tech
[Astro] Content Collections + glob loader で Markdown ブログを作る

WordPress から移行したこのブログは、記事を src/content/blog/ に Markdown で置いています。 それを型安全に扱うために使っているのが Astro の Content Collectionsglob loader です。

コレクションを定義する

src/content.config.ts でコレクションを定義します。loaderglob() を指定すると、好きなディレクトリの Markdown をまとめて読み込めます。schema に Zod スキーマを書くと、frontmatter の型チェックまでやってくれます。

// src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    description: z.string().optional(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    category: z.enum(['tech', 'marketing', 'office', 'music', 'life']),
    tags: z.array(z.string()).default([]),
    thumbnail: z.string().optional(),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

ポイントをいくつか。

  • pubDate: z.coerce.date() にすると、frontmatter に pubDate: 2026-06-18 と書くだけで Date 型に変換してくれる。
  • category: z.enum([...]) で、決めたカテゴリ以外を弾ける。タイプミスや想定外カテゴリをビルド時に検出できるのが気持ちいい。
  • default(...) を付けておけば、frontmatter で省略してもデフォルト値が入る(tagsdraft など)。

一覧を取得する

記事の取得は getCollection('blog') です。draft を除外して日付降順に並べる、といった定番処理はこんな感じ。

---
import { getCollection } from 'astro:content';

const posts = (await getCollection('blog'))
  .filter((p) => !p.data.draft)
  .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></li>
  ))}
</ul>

glob loader 版では、各エントリの ID は**ファイル名(拡張子なし)**になります。2026-06-18-astro-content-collections-glob.md なら post.id2026-06-18-astro-content-collections-glob。これをそのまま URL のスラッグに使っています。

記事ページを生成する

動的ルート src/pages/blog/[slug].astro で、各記事のページを静的生成します。render() で本文コンポーネントを取り出して描画します。

---
// src/pages/blog/[slug].astro
import { getCollection, render } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts
    .filter((p) => !p.data.draft)
    .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>

getStaticPathsdraft を除外しておくと、下書き記事はページ自体が生成されません(URL を直打ちしても 404)。逆に言うと、ローカルで下書きをプレビューしたいときは一時的に draft: false にする必要があります。ここは少しだけ注意点。

まとめ

  • コレクション定義 + Zod スキーマで frontmatter が型安全になる
  • glob loader なら ファイル名がそのまま ID(= スラッグ)
  • 一覧は getCollection、本文は renderContent

Markdown を置くだけで型チェック付きのブログになるので、個人ブログの記事管理にはちょうど良かったです。

Astro, Markdown