Next.js App RouterとPagefindで爆速のサイト内検索を実装する
Next.js (App Router) のブログに Pagefind で全文検索機能を実装する手順を紹介します。public ディレクトリへのインデックス出力設定や、クライアントサイドでの動的インポートなど、実装に必要なポイントをコード付きで解説します。
目次
はじめに
最近、ブログのアーキテクチャを MDX から Markdown ベースのシンプルな構成へ移行しました。
それに伴い、過去の記事をすべてこのブログに集約した結果、記事数は 90 ページを超える規模になりました。
正直なところ、自分で過去の記事を読み返すことはあまりないのですが、今後も知見を書き溜めていく上で検索機能が必要だと感じ、ブログ検索機能を作成することにしました。
調査を進めると、ビルド時に静的インデックスを作成し、クライアントサイドで検索を実行する Pagefind というライブラリが非常に優秀であることが分かりました。
有名な技術ブログでも採用されており、静的サイトにおける検索機能として一定の信頼性があると判断しました。
今回は、Next.js (App Router) 環境に Pagefind を導入する手順を紹介します。
セットアップ
Pagefind は、静的サイトジェネレータ(SSG)で生成された HTML ファイルを解析し、検索用インデックスを作成します。
今回はランタイムに Bun を使用しているため、package.json のスクリプトは以下のようになります。
{
"scripts": {
"dev": "bun run --bun next dev",
"build": "bun run --bun next build",
"postbuild": "pagefind --site .next --output-path public/pagefind",
"start": "next start"
}
}上記のコマンドでは、postbuild フックを使って Next.js のビルド成果物(.next)をスキャンし、インデックスの出力先(--output-path)を public/pagefind に指定しています。
通常、SSG であればビルド成果物の中に含めてしまえば良いのですが、Next.js の場合、public ディレクトリ配下に置いたファイルが静的アセットとして配信されるという仕様があります。
ここにインデックスファイルや pagefind.js を出力することで、ブラウザ(クライアントサイド)から /pagefind/pagefind.js として直接アクセスが可能になり、スムーズに検索スクリプトをロードできます。
実際にこのブログで検索したときのデモです。見ての通り爆速で記事検索ができていることが確認できます。

実装時の注意点
Next.js (webpack) の環境では、ビルド時に存在しないファイルを import しようとするとエラーになります。
Pagefind のスクリプトは postbuild で生成されるため、/* webpackIgnore: true */ を指定して webpack のバンドル対象から外し、ブラウザランタイムで直接 public ディレクトリから読み込むようにしています。

また、Pagefind は .next ディレクトリ内の成果物をスキャンするため、検索結果の URL が /server/app/blog/post-1.html のような内部パスで返却されます。
実際に開発者ツールを確認して、/blog/github-actions-security-basics-minimum-measures の URL を見てみると、server/app/blog/github-actions-security-basics-minimum-measures.html と想定とは異なるファイルパスが設定されていることが確認できます。

このパスになっていると、実際に画面遷移した際にページが見つからず、404 エラーとなってしまうので、以下のようなコードで /server/app/ と .html 拡張子も削除します。
function normalizePagefindUrl(pagefindUrl: string): string {
// `/server/app/`を削除して、`.html`拡張子も削除
return pagefindUrl
.replace(/^\/server\/app\//, '/') // /server/app/ を削除
.replace(/\.html$/, ''); // .html を削除
}検索ノイズの除去
ブログ全体を検索対象にしたい一方で、トップページにある「自己紹介」などが検索結果に出てくるとノイズになります。
Pagefind は data-pagefind-ignore 属性を付与することで、特定の要素をインデックス対象から除外できます。
私のブログでは以下のように自己紹介を記載しているページがあります。この部分は検索の対象外にしたいため、親要素に data-pagefind-ignore を設定します。
<div data-pagefind-ignore>
<h1 className='pb-6 text-4xl font-semibold tracking-wide md:text-[40px]'>
私について
</h1>
<div className='mt-4 space-y-1'>
<h2 className='text-2xl font-semibold'>スイ</h2>
<p>
東京都で活動するエンジニア。名前の由来は、目の前にあったサントリーの天然水から命名しています。
</p>
<p>健康第一をモットーにしており、一年以上ほぼ毎日朝活🌅しています。</p>
</div>
</div>実際に設定してみると、設定前ではトップページの内容が検索にヒットしてしまいますが、設定後は自己紹介文に含まれる単語を検索しても、検索対象外になっていることが確認できます。
設定前

設定後

まとめ
- Pagefind はビルド時に静的インデックスを作成し、クライアントサイドで検索を実行する検索機能ライブラリ
webpackIgnoreを使って動的にスクリプトを読み込むpublicディレクトリ経由で静的アセットとして配信する- 内部パスを正規化して正しい URL に変換する
参考
Next.jsとPagefindで検索機能を簡単実装してみた | 株式会社LIG(リグ)|DX支援・システム開発・Web制作liginc.co.jp
静的サイト向けの全文検索エンジンと UI ライブラリの Pagefindazukiazusa.dev
Next.js App RouterでPagefindを使うときのあれこれmh4gf.dev
おまけ
作成したコードを載せてもよかったのですが、フロントエンドを触るのが久しぶりすぎたので気になる人だけ見てください…(結構汚いです)
本当は検索で引っかかった記事アイコンのキャッシュとかもできたらよいですね。