ota2000
2 min read

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件を超えたら自動で出る。

全部静的ファイルで完結するので、外部サービスへの依存もランタイムのサーバーもない。