今回は、Astroの目玉機能である「コンテンツコレクション(Content Collections)」を導入して、Markdown(.md)形式で書いた記事を管理できるようにしていきます。
コンテンツコレクションは、記事の「型(ルール)」を厳密に決めることで、書き間違いを防ぎ、データを取り出しやすくする素晴らしい機能です。
※なお、設定ファイルやblog記事の作成にはVS Codeを使用しています。
contentディレクトリの作成
最初に、記事を保存する専用のディレクトリ「src/content」を作ります。
私の環境ではAstroを「~/project/astro-dev」にインストールしているので、以下のようにディレクトリを作成しました。
$ cd ~/project/astro-dev/src/ $ mkdir content
また、blogとして記事を書いていく予定なので、それ用のディレクトリ(今回は年別のディレクトリ)もあわせて作成しておきます。
$ cd content $ mkdir -p blog/2026
スキーマ(ルール)の作成
次に、「このブログ記事には、どんなデータが絶対に必要か」というルール(スキーマ)を決めます。
これを決めることで、Astroが「日付が入力されていない」など、データが足りない場合にエラーを出して教えてくれるようになります。
「src/content/」ディレクトリの直下に、「config.ts」というTypeScriptファイルを作成し、以下の内容を記述します。
import { z, defineCollection } from 'astro:content';
// blogコレクションのルール(スキーマ)を定義
const blogCollection = defineCollection({
type: 'content', // Markdownなどのコンテンツを扱うことを指定
schema: z.object({
title: z.string(), // title は必須で「文字列」であること
pubDate: z.date(), // pubDate は必須で「日付」であること
description: z.string(), // description は必須で「文字列」であること
author: z.string(), // author は必須で「文字列」であること
}),
});
// コレクションをエクスポートしてAstroに認識させる
export const collections = {
'blog': blogCollection,
};
「image: z.string().optional(),」といったように「optional()」をつけると、入力しなくてもOKになります。
記事を表示するページ(動的ルーティング)を作る
Astroの機能である「動的ルーティング(Dynamic Routing)」を使って、作ったMarkdown記事をWebページとして表示させる仕組みを作ります。
「src/pages/blog/」ディレクトリを作成します。
$ mkdir -p ~/project/astro-dev/src/pages/blog
そこに、[...slug].astroという特殊な名前のファイルを作成します。
ファイル名の「...(ピリオド3つ)」は、スラッシュを含む複数階層のURLに対応できる「レストパラメーター」という仕組みです。
今回は記事を「src/content/blog/2026/xxx.md」といった深い階層に作成するため、この記述が必要になります。
ファイルの内容は以下のとおりです。
---
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
// getStaticPaths: 「どのURL(ページ)を生成するか」をAstroに教える必須の関数
export async function getStaticPaths() {
const blogEntries = await getCollection('blog'); // blogフォルダの記事を全部取ってくる
return blogEntries.map(entry => ({
params: { slug: entry.slug }, // URLの一部(ファイル名)になる部分
props: { entry }, // そのページで使うデータ(記事の全情報)を渡す
}));
}
// 上の getStaticPaths から渡されたデータを受け取る
const { entry } = Astro.props;
// Markdownの本文をHTMLに変換(レンダリング)する
const { Content } = await entry.render();
---
<BaseLayout pageTitle={entry.data.title} pageDescription={entry.data.description}>
<article>
<h1>{entry.data.title}</h1>
<p>公開日: {entry.data.pubDate.toLocaleDateString('ja-JP')}</p>
<p>著者: {entry.data.author}</p>
<hr />
<Content />
</article>
</BaseLayout>
問題が発生
ここで、VS Code上などで「プロパティ 'render' は型 'never' に存在しません」といったエラーが発生することがあります。
Astroは、先ほど作成した「config.ts」(スキーマ設定)を見つけると、裏側で自動的に専用の「型定義ファイル」(.astro という隠しディレクトリ内)を作成します。
しかし、新しくcontentフォルダを作った直後などは、Astroの開発サーバーがこの変更にうまく気付けず、型定義ファイルの生成が遅れることがあります。
その結果、「blog というコレクションなんて知らない(=空っぽの never 型だ)」と判定されてしまい、「never 型には render なんて便利な機能はついてない」と怒られてしまっている状態です。
解決策
これは設定ミスではないため、Podmanコンテナ(開発サーバー)を再起動して設定を再読み込みさせるとなおります。
$ cd ~/project/astro-dev $ podman-compose down $ podman-compose up -d
※コンテナを再起動してもVS Code上の赤波線が消えない場合は、VS Code自体も一度再起動してみてください。
記事の作成と動作確認
「src/content/blog/2026/」に「first-post.md」という名前で記事を作成します。
--- title: "コンテンツコレクションを使って記事を作成" pubDate: 2026-03-04 description: "コンテンツコレクションを使った最初の記事です。" author: "tamohiko" --- # コンテンツコレクションへようこそ! ここから下は、普通のMarkdownで本文を書いていきます。 ## 今日のやったこと - ディレクトリの作成 src/content - スキーマの定義 src/content/config.ts - src/pages/blog/[...slug].astro ファイルの作成 - Markdown記事の作成 src/content/blog/2026/first-blog.md
ブラウザで「http://localhost:4321/blog/2026/first-post」にアクセスし、記事が表示されることを確認してください。
URLを短くスッキリさせる(応用編)
現在は「src/content/blog/2026」ディレクトリに記事を書いていく形になっていますが、毎日ブログを更新すると仮定した場合、1年で1つのディレクトリ内に365個の.mdファイルと画像データが格納されてしまい、管理がしづらくなってしまいます。
そのため、今後は「src/content/blog/2026/03」といったように「年/月」の形でディレクトリを分けて管理していきたいと考えました。
しかしそうすると、ページのURLが以下のように更に長くなってしまいます。
http://localhost:4321/blog/2026/03/first-post
これを、ディレクトリ管理は階層化しつつ、URLは以下のようにスッキリと短い形式に変更させる設定を行います。
http://localhost:4321/blog/first-post
ファイル名とコードの変更
URLの形式が blog/ 以下にディレクトリを含まない「http://localhost:4321/blog/first-post」という形になるので、「src/pages/blog/[...slug].astro」ファイルの名前を「src/pages/blog/[slug].astro」(...を削除)に変更します。
$ cd ~/project/astro-dev/src/pages/blog/ $ mv \[...slug\].astro \[slug\].astro
次に「src/pages/blog/[slug].astro」の中身を以下のように書き換えます。
---
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const blogEntries = await getCollection('blog');
return blogEntries.map(entry => {
// entry.slug は "2026/03/first-post" のようになっている
// split('/') で ["2026", "03", "first-post"] という配列に分割する
// pop() で一番最後(ファイル名)の "first-post" だけを取り出す
const cleanSlug = entry.slug.split('/').pop();
return {
// 取り出したファイル名だけをURLとしてAstroに教える
params: { slug: cleanSlug },
props: { entry },
};
});
}
// 以下の表示部分は変更なしでOKです
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<BaseLayout pageTitle={entry.data.title} pageDescription={entry.data.description}>
<article>
<h1>{entry.data.title}</h1>
<p>公開日: {entry.data.pubDate.toLocaleDateString('ja-JP')}</p>
<p>著者: {entry.data.author}</p>
<hr />
<Content />
</article>
</BaseLayout>
動作確認
[slug].astroファイルを保存したら、ブラウザで以下のURLにアクセスして、短いURLで記事が表示されることを確認してください。
http://localhost:4321/blog/first-post





コメント