本サイトのブログ記事が増えてきたので、タグのインデックスページを追加しました。
ブログのインデックスページの下部にあるタグ一覧や、各ブログ記事のタイトル上のタグからアクセスできます。

実装方法の検討
見出し「実装方法の検討」本サイトのフレームワークには Astro を使用していますが、タグページの実装にあたり、Astro のチュートリアルページと、エビスコムさんによる電子書籍『Astro v2とTinaCMSでシンプルに作るブログサイト』を参考にしました。
- Generate tag pages | Astro Documentation(外部リンクを開く)
- 『Astro v2とTinaCMSでシンプルに作るブログサイト』(編著:エビスコム)(外部リンクを開く)
まず、Astro のチュートリアルページですが、こちらではすべての記事からタグを取得し、ユニークな配列に変換して、その配列をもとにタグページを生成する方法が紹介されています。
しかし、この方法ではタグ名がそのまま URL に使われるため、スペースや日本語を使用したときに、パーセントエンコーディング(percent-encoding)がおこなわれます1。
このように、自由にスラッグを指定できないことに加えて、タグページごとに見出しのテキストラベルといった独自のデータを持たせたかったので、この方法は見送りました。
一方の『Astro v2とTinaCMSでシンプルに作るブログサイト』では、「2.7 カテゴリーインデックスページの作成」のセクションで、外部ファイルでタグを管理する方法が紹介されています。この方法であれば自由にデータを持たせられるので、今回はこちらをベースに実装することにしました。

テンプレートファイル構成
見出し「テンプレートファイル構成」タグページを実装するにあたり、ブログページを以下のファイル構成にしました。
src/pages/blog/
├── [...page].astro // ブログインデックス
├── [...slug].astro // ブログ記事
└── tags/[tagSlug]/[...page].astro // タグページ
今回追加したのは、末尾の src/pages/blog/tags/[tagSlug]/[...page].astro
です。
[tagSlug]
の部分は、getStaticPaths()
関数でスラッグを指定することで、動的なルーティングを実現できます。
[...page].astro
はページネーションを有効にするときのファイル名です。getStaticPaths()
関数内で paginate()
関数を返すことで指定した件数ごとのページ分割が実現できます。
以下は、タグページのテンプレートの、getStaticPaths()
に関する部分を抜き出したコードです(一部簡略化)。
---
import { getCollection } from 'astro:content';
// タグ一覧のデータを取得
import tagsList from '@data/blog/tags.yml';
export async function getStaticPaths({ paginate }) {
// Content Collections からブログ記事を取得(下書きを除外)
const posts = await getCollection('blog', ({ data }) => !data.draft);
// ブログ記事を公開日の新しい順にソート
const sortedPosts = posts.sort((a, b) => Date.parse(b.data.pubDate) - Date.parse(a.data.pubDate));
// タグがいずれかの記事に含まれているかをチェック(空の表示を回避)
const filteredTags = tagsList.filter((tag) => {
return posts.some((post) => post.data.tags.includes(tag.tagName));
});
return filteredTags.map((tag) => {
const { tagName, tagSlug, titleJa, titleEn } = tag;
// タグを含むブログ記事に絞り込み
const taggedPosts = sortedPosts.filter((post) => post.data.tags.includes(tagName));
// `paginate()` 関数を返す
return paginate(taggedPosts, {
params: { tagSlug },
props: { tagName, titleJa, titleEn },
pageSize: 10
});
});
}
const { page, tagName, titleJa, titleEn } = Astro.props;
---
import tags from '@data/blog/tags.yml'
では、タグ一覧のデータを格納しているファイルを取得していますが、そのデータファイルの中身を見ていきます。
データファイル
見出し「データファイル」タグ一覧を管理するデータファイルは以下のような構成です。データ管理を容易にするために、YAML 形式を採用することにしました2。
-
tagName: アクセシビリティ
tagSlug: accessibility
titleJa: アクセシビリティ
titleEn: Accessibility
-
tagName: Astro
tagSlug: astro
titleJa: アストロ
titleEn: Astro
-
tagName: CSS
tagSlug: css
titleJa: シーエスエス
titleEn: CSS
同一の値や似た値が並びますが、それぞれ以下の役割を持っています。
key | value |
---|---|
tagName | ページタイトルやテキストラベル |
tagSlug | スラッグ |
titleJa | タグページの見出し(日本語表記) |
titleEn | タグページの見出し(英語表記) |
指定した値は、タグページの URL や各テキストラベル、メタデータなどに反映されます。

tags.yml
で指定した値が適用されるtitleEn
は CSS の text-transform: uppercase
で大文字表記に変換しています。
Zod を使用したバリデーション
見出し「Zod を使用したバリデーション」タグ一覧のデータ(tags.yml
)は手動で管理しているため、ブログ記事を作成するときに、存在しないタグを指定したり、タグ名の表記揺れやスペルミスが発生する可能性があります。
Astro 2.0 から導入された Content Collections では、Zod を使用して Markdown / MDX の Front Matter の型をバリデーションすることができるようになりました。
この機能を活用して、Front Matter に記述したタグが tags.yml
に含まれていないときに、エラーメッセージを表示するように設定しました。
import { z, defineCollection } from 'astro:content';
// タグ名だけの配列を作成
import tagsList from '@data/blog/tags.yml';
const tagNames = tagsList.map((tag: { tagName: string }) => tag.tagName);
// ブログのデータ型を定義
const blogCollection = defineCollection({
schema: z.object({
// ...
// タグは文字列の配列で `tags.yml` 内のタグ名に含まれる必要がある
tags: z.string().array().refine((arr) => arr.every((tag) => tagNames.includes(tag)), {
message: '`@data/blog/tags.yml` に存在しないタグが含まれています。',
}),
// ...
})
});
export const collections = {
'blog': blogCollection,
}
refine
メソッドを使用して、ブログ記事で指定したタグ名がタグ一覧の名前(tagName
)に含まれているかをチェックし、第二引数の message
でエラーメッセージを指定しています。
例えば、ブログ記事で存在しないタグを指定すると以下のようなエラー画面が表示されます。

message
で指定したエラーメッセージが表示される今後の課題
見出し「今後の課題」ここまでのプロセスを経て、タグページを実装することができました。しかし、タグを手動で管理しているため、タグ一覧のデータとブログ記事に指定されているタグの双方向でチェックする必要があり、ロジックが複雑になってしまいました。
テンプレート内で以下のチェックをして、要素の出し分けやエラー表示を判定しています。
- ブログ記事に指定したタグが、タグ一覧のデータに存在するかのチェック
- タグに含まれるブログ記事が存在するかのチェック
前者は、すでに説明した Zod を使用したバリデーションで、後者は、タグページでブログ記事が空(0 件)の場合に表示しない処理や、タグ一覧にブログ記事が空のタグを表示させない処理です。
これらの処理は、ブログ記事やタグの数が増えるごとに、デプロイ先でのビルド時間に影響を及ぼすかもしれませんが、ひとまず様子を見つつ、よりよい方法が見つかれば調整していきたいです。
脚注
-
パーセントエンコーディングとは、URL に使えない文字を変換する処理で、例えば半角スペースであれば
%20
に符号化されます。 ↩ -
Astro で YAML 形式のデータをインポートするには、プラグインをインストールする必要があります。(Installing a Vite or Rollup plugin | Astroドキュメント) ↩