% cd ..

Pagefind でブログに検索機能をつけた

Pagefind でブログに検索機能をつけた

検索が欲しくなった

記事が少しずつ増えてきて、自分でも「あの話どこに書いたっけ」となることが多くなりました。 タグで絞り込めるとはいえ、キーワードでサッと引ける検索があると便利です。

ただ、このブログは完全無料スタックで運用しているので、検索のために有料サービスを足すのは避けたい。 サーバーサイドの処理も増やしたくない。できればビルド時に完結する仕組みがベストです。

候補を比較した

個人ブログの静的サイトに検索をつける選択肢は、だいたいこのあたりに絞られます。

ツール方式日本語コスト特徴
Pagefindビルド時インデックス生成標準対応
CJK トークナイザ
無料軽量(~6KB)、多言語対応
Oramaブラウザ / Edge 全文検索プラグインで対応無料高機能だがインデックス構築が自前
Fuse.jsクライアント fuzzy search無料記事が増えると JSON 全量ロードが重い
Flexsearchクライアント全文検索無料Fuse.js と同じスケーラビリティの問題
AlgoliaSaaS(DocSearch 無料枠)対応条件付無料高性能だが外部依存

Pagefind を選んだ理由

Pagefind は CloudCannon が開発している OSS で、静的サイトのビルド成果物(HTML)からインデックスを自動生成するツールです。

選んだ決め手はこのあたり。

  • 無料! — MIT ライセンスの OSS。ホスティング側のコストも増えない
  • 日本語が動く — CJK(中国語・日本語・韓国語)トークナイザを内蔵。追加設定なしで日本語の検索に対応
  • ビルド時に完結 — Vercel にデプロイ時にインデックスをファイルとして生成。検索時 API コールなどはなし
  • 多言語サイトの相性 — このブログは /ja//en/ でコンテンツを分けているのでディレクトリ構造をそのまま活かせる
  • 爆速 — 初期ペイロードが小さい。検索インデックスは実際の検索時に必要な分だけ分割して遅延ロードする

Fuse.js や Flexsearch も悪くはないですが、記事数が増えたときにクライアントへ全データを送る設計がネックになります。Algolia は機能的には申し分ないものの、無料枠の条件管理が面倒で、個人ブログのスタックとしては重すぎる。

Next.js + Vercel での構成

インストール

npm install pagefind

ビルドスクリプト

package.jsonbuild コマンドを変更します。

{
  "scripts": {
    "build": "next build && pagefind --site .next/server/app --output-path public/_pagefind"
  }
}

next build が終わった後に、Pagefind が .next/server/app(ビルド済み HTML の出力先)を走査してインデックスを生成し、public/_pagefind/ に配置します。Vercel はビルド時にこのコマンドを実行するので、デプロイ後は /_pagefind/pagefind.js として静的に配信されます。

インデックス作成を行うコア部分は Rust で書かれており、とにかく爆速で完了します。 この個人ブログくらいの規模だとマジで秒でインデックス作成が完了するので、実質ノーコストです。 使い始める前は、インデックス作成に時間が掛かったりすると Vercel のビルド無料枠とか大丈夫かな、とか心配していたのですが、杞憂でした。

public/_pagefind/ はビルドの度に再生成されるので .gitignore に追加しておきます。

インデックス対象の制御

何も指定しないと <body> 全体がインデックスされてしまい、サイドバーやヘッダーのテキストまで検索にヒットします。
記事本文の <div>data-pagefind-body 属性を追加して、インデックス対象を記事本文だけに絞ります。

<div className="prose prose-invert max-w-none" data-pagefind-body>
  {parse(post.contentHtml, parserOptions)}
</div>

これで Pagefind は data-pagefind-body が付いた要素だけをインデックスするようになります。
実際にビルドしてみると、82 ファイルから 16 ページ(記事本文のみ)に絞り込まれました。

検索コンポーネント

Pagefind にはデフォルトの検索 UI が付属していますが、このブログのテーマに合わせたかったので、自前のコンポーネントを書きました。

  1. 虫眼鏡アイコンのクリックで検索パネルが開く
  2. 初回オープン時に /_pagefind/pagefind.js を動的 import でロード
  3. キー入力のたびに pagefind.search() を呼び、結果をリスト表示
  4. Escape で閉じる。/ キーでトグル
// ランタイムで /_pagefind/pagefind.js を動的ロード
// ビルド時には存在しないため、パスを変数経由で渡して TS の静的解析を回避
const path = "/_pagefind/pagefind.js";
const pf = await import(/* webpackIgnore: true */ path);
await pf.init();

Pagefind の JS はビルド後に生成されるため、TypeScript の静的解析では「モジュールが見つからない」とエラーになります。パスを変数に入れて import() に渡すことで回避しています。

ローカル開発での制約

Pagefind はビルド済み HTML からインデックスを作るので、next dev(開発サーバー)では検索が動きません。ローカル環境でも検索を試すには:

npm run build   # next build + pagefind でインデックス生成
npm start       # localhost:3000 で本番ビルドを起動

開発中は検索以外の作業は next dev で、検索の確認だけ build → start でやる運用です。

日本語で使ってみて

一般的な日本語(「翻訳」「対応」「対策」など)の検索は問題なく動きます。ただ、一部のカタカナ語で意図しない結果が混ざるケースがありました。原因を掘り下げたら Pagefind の日本語処理の構造的な話になったので、別記事にまとめました。

まとめ

  • Pagefind は静的サイトに検索を追加する最もシンプルな選択肢
  • ビルドコマンドに 1 行追加するだけで動く
  • 日本語も対応済み(ただし精度には構造的な制約あり)
  • JS ~6KB + 遅延ロードで、パフォーマンスへの影響はほぼゼロ
  • Vercel との相性も良く、追加コストなし

おまけ: Pagefind を作っている CloudCannon

Pagefind の開発元である CloudCannon は、同名の Git-based CMS を本業として手がけています。Next.js との親和性も高く、特に WYSIWYG の UI 編集機能が強力なのが好評のようですが、こちらは有料。スタンダードプランでも $49/month (USD) と、個人ブログ向きのお値段ではありませんでした。

OSS の Pagefind は無料で使えるのがありがたい限りです。