Astro ブログに全文検索とタグ機能を追加する
記事が増えてきたときに検索がないと困る。Algolia のような外部サービスを入れるほどの規模ではないので、ビルド時に JSON を生成してクライアントサイドで検索する方式にした。
タグ
Content Collection のスキーマに tags を追加した。
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
draft: z.boolean().default(false),
tags: z.array(z.string()).default([]),
})
frontmatter に tags: ["Cloudflare", "Terraform"] と書く。タグ別の一覧ページは /blog/tag/[tag].astro で動的に生成される。
ブログ一覧にもタグをピル状に並べて、クリックでフィルタできるようにした。
検索
ビルド時に全記事のメタデータを JSON として出力するエンドポイントを作った。
// src/pages/blog/search.json.ts
export async function GET() {
const posts = (await getCollection('blog'))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const data = posts.map((post) => ({
id: post.id,
title: post.data.title,
description: post.data.description,
date: post.data.date.toISOString(),
dateStr: post.data.date.toLocaleDateString('ja-JP'),
tags: post.data.tags,
}));
return new Response(JSON.stringify(data));
}
/blog/search.json に全記事のタイトル・説明文・タグが入る。クライアントでこれを fetch して部分一致検索する。
const matched = posts.filter((p) =>
p.title.toLowerCase().includes(q) ||
p.description.toLowerCase().includes(q) ||
p.tags.some((t) => t.toLowerCase().includes(q))
);
JSON は初回の検索時に1回だけ取得してキャッシュする。検索中はページネーションの記事リストを隠して、検索結果だけ表示する。
ページネーション
Astro の paginate() で10件ずつ分割している。
---
export const getStaticPaths = (async ({ paginate }) => {
const posts = (await getCollection('blog'))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
return paginate(posts, { pageSize: 10 });
}) satisfies GetStaticPaths;
---
今は記事が少ないのでページ分割は見えないが、10件を超えたら自動で出る。
全部静的ファイルで完結するので、外部サービスへの依存もランタイムのサーバーもない。