ブログサイトのロゴsui Tech Blog

Next.js App RouterとPagefindで爆速のサイト内検索を実装する

Next.js (App Router) のブログに Pagefind で全文検索機能を実装する手順を紹介します。public ディレクトリへのインデックス出力設定や、クライアントサイドでの動的インポートなど、実装に必要なポイントをコード付きで解説します。

はじめに

最近、ブログのアーキテクチャを MDX から Markdown ベースのシンプルな構成へ移行しました。
それに伴い、過去の記事をすべてこのブログに集約した結果、記事数は 90 ページを超える規模になりました。

正直なところ、自分で過去の記事を読み返すことはあまりないのですが、今後も知見を書き溜めていく上で検索機能が必要だと感じ、ブログ検索機能を作成することにしました。

調査を進めると、ビルド時に静的インデックスを作成し、クライアントサイドで検索を実行する Pagefind というライブラリが非常に優秀であることが分かりました。
有名な技術ブログでも採用されており、静的サイトにおける検索機能として一定の信頼性があると判断しました。

pagefind.app

Pagefind | Pagefind — Static low-bandwidth search at scale

Pagefind is a fully static search library that aims to perform well on large sites, while using as little of your users’ bandwidth as possible, and without hosting any infrastructure.

今回は、Next.js (App Router) 環境に Pagefind を導入する手順を紹介します。

セットアップ

Pagefind は、静的サイトジェネレータ(SSG)で生成された HTML ファイルを解析し、検索用インデックスを作成します。
今回はランタイムに Bun を使用しているため、package.json のスクリプトは以下のようになります。

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 ディレクトリ配下に置いたファイルが静的アセットとして配信されるという仕様があります。

nextjs.org

File-system conventions: public | Next.js

Next.js allows you to serve static files, like images, in the public directory. You can learn how it works here.

ここにインデックスファイルや pagefind.js を出力することで、ブラウザ(クライアントサイド)から /pagefind/pagefind.js として直接アクセスが可能になり、スムーズに検索スクリプトをロードできます。

実際にこのブログで検索したときのデモです。見ての通り爆速で記事検索ができていることが確認できます。
image

実装時の注意点

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

petemillspaugh.com

Add search to your Next.js static site with Pagefind

Pete Millspaugh's digital garden

また、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 と想定とは異なるファイルパスが設定されていることが確認できます。
image
このパスになっていると、実際に画面遷移した際にページが見つからず、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 属性を付与することで、特定の要素をインデックス対象から除外できます。

pagefind.app

Configuring what content is indexed | Pagefind — Static low-bandwidth search at scale

You can control what content on your site is indexed via data-pagefind-* tags on your HTML elements. Limiting what sections of a page are indexed By default, Pagefind starts indexing from your element.

私のブログでは以下のように自己紹介を記載しているページがあります。この部分は検索の対象外にしたいため、親要素に data-pagefind-ignore を設定します。

src/app/page.tsx
<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>

実際に設定してみると、設定前ではトップページの内容が検索にヒットしてしまいますが、設定後は自己紹介文に含まれる単語を検索しても、検索対象外になっていることが確認できます。

設定前

image

設定後

image

まとめ

  • Pagefind はビルド時に静的インデックスを作成し、クライアントサイドで検索を実行する検索機能ライブラリ
  • webpackIgnore を使って動的にスクリプトを読み込む
  • public ディレクトリ経由で静的アセットとして配信する
  • 内部パスを正規化して正しい URL に変換する

参考

liginc.co.jp

Next.jsとPagefindで検索機能を簡単実装してみた | 株式会社LIG(リグ)|DX支援・システム開発・Web制作

静的サイトに特化した検索ライブラリ「Pagefind」を使って、ページ内検索機能を簡単4ステップで実装してみました。スクラッチで検索機能を開発するのに比べて、短い工数で何十倍も楽に制作できるので、みなさんぜひお試しください!

azukiazusa.dev

静的サイト向けの全文検索エンジンと UI ライブラリの Pagefind

Pagefind は、静的サイト向けの全文検索エンジンと UI ライブラリです。特定のフレームワークに依存しない JavaScript ライブラリとして実装されており、静的サイトジェネレーターで生成された HTML ファイルに対して検索機能を追加できます。

mh4gf.dev

Next.js App RouterでPagefindを使うときのあれこれ

このブログサイトでサイト内検索機能を追加しました。検索が必要なほど記事数はないので完全に自己満足。右下の⌘ボタンか、Cmd + Kキーを押すと検索フォームが表示されるので、テキストを入力し試してみてください。

petemillspaugh.com

Add search to your Next.js static site with Pagefind

Pete Millspaugh's digital garden

おまけ

作成したコードを載せてもよかったのですが、フロントエンドを触るのが久しぶりすぎたので気になる人だけ見てください…(結構汚いです)
本当は検索で引っかかった記事アイコンのキャッシュとかもできたらよいですね。

github.com

sui-blog/src/components/feature/search/search-dialog.tsx at main · Suntory-N-Water/sui-blog

sui Tech Blog. Contribute to Suntory-N-Water/sui-blog development by creating an account on GitHub.

...

関連記事