PR
PR

【Astro入門】コンテンツコレクションでMarkdown記事を管理・表示する方法とURL(パーマリンク)の最適化

記事内に広告が含まれています。

今回は、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

コメント

タイトルとURLをコピーしました