Pagefind は静的サイトに特化した軽量な検索ライブラリです。SSG サイト向けの Cloud CMS を提供する CloudCannon 社によって開発されています。
この Pagefind について、以前に Starlight の記事を作成したときに知ったのですが、いつのまにか安定バージョン(v1)になっていたので、試しにブログ記事の検索機能を実装してみました。
以下はライブデモで、本サイトのブログ記事のテキスト検索ができます。
導入方法
見出し「導入方法」Pagefind は静的サイトであれば(HTML ファイルさえあれば)、基本的にどのようなサイトであっても以下の 2 ステップで導入できます。
- Pagefind をインストールし、検索用ファイルを生成する
- Pagefind の UI を読み込み、インスタンスを作成する
1. Pagefind をインストールし、検索用ファイルを生成する
見出し「1. Pagefind をインストールし、検索用ファイルを生成する」まず、検索用のインデックスファイルを生成します。本サイトのフレームワークは Astro を使用しているので、Astro のビルドと連携させるために npm パッケージをインストールします。
npm install pagefind
次に、Astro のビルド後に Pagefind CLI を実行するように、npm-scripts を記述します。
{
"scripts": {
"build": "astro build && pagefind --site dist"
}
}
これで、npm run build
コマンドを実行すると Astro によって生成された HTML ファイルの内容に基づいて、Pagefind で使用するインデックスファイルが生成されます。なお、生成されるインデックスは、HTML ファイルに指定した lang
属性の値に基づいて言語が判別されます。
コマンド実行後、CLI には以下のような結果が表示されます。
Indexed 1 language
Indexed 38 pages
Indexed 4816 words
Indexed 0 filters
Indexed 0 sorts
Astro 以外の SSG でも、基本的にはこのような流れになると考えられます。
2. Pagefind の UI を読み込み、インスタンスを作成する
見出し「2. Pagefind の UI を読み込み、インスタンスを作成する」続いてフロント側の指定です。Pagefind の UI を読み込んで検索フォームを表示します。
前述の「1」で Pagefind CLI を実行しましたが、そのときに --site
で指定したディレクトリ以下に pagefind
ディレクトリが生成されます。そこに Pagefind UI の CSS(pagefind-ui.css
)と JS(pagefind-ui.js
)が含まれています。
この CSS と JS を読み込んでから、new
演算子でインスタンスを作成するだけで完了です。
<link rel="stylesheet" href="/pagefind/pagefind-ui.css">
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
new PagefindUI({ element: "#search" });
});
</script>
これで、デフォルト状態の Pagefind の検索フォームが使用できるようになりました。
CLI で Pagefind のコマンド npx pagefind --site dist --serve
、もしくは Astro であれば npx astro preview
で、ローカルサーバを立ち上げて動作確認ができます。
npx pagefind --site dist --serve
オプション指定
見出し「オプション指定」ここからは、Pagefind をカスタマイズしていきます。
Pagefind UI をインスタンス化する際にオプションを指定できますが、代表的な項目をいくつかピックアップします。
プロパティ | 値(例) | 内容 |
---|---|---|
element | #search | 検索フォームを表示するセレクタ(必須) |
baseUrl | / | ベース URL |
pageSize | 5 | 1 ページに表示する検索件数 |
showSubResults | false | ページ内の各見出しの検索結果の表示 |
showImages | true | サムネイル画像の表示 |
excerptLength | 30 | 概要テキストの長さ |
debounceTimeoutMs | 300 | ユーザが入力してから結果を表示するまでの待ち時間(ms) |
また、translations
オブジェクトで、各種ラベルの表記を変更することができます。日本語のデフォルトのラベルは以下のとおりです。
以下は現時点での本サイトでの設定ですが、各見出しの検索結果やサムネイル画像は無効にした、最小限の構成としています。
new PagefindUI({
element : '#search',
baseUrl : '/blog/',
pageSize : 10,
excerptLength : 50,
showSubResults : false,
showImages : false,
translations : {
placeholder : 'ブログ記事を検索',
clear_search : 'クリア',
load_more : 'More',
search_label : '',
zero_results : '「[SEARCH_TERM]」に一致する記事は見つかりませんでした 🙇♂️',
many_results : '「[SEARCH_TERM]」の検索結果([COUNT] 件)',
one_result : '「[SEARCH_TERM]」の検索結果([COUNT] 件)',
searching : '「[SEARCH_TERM]」を検索しています...',
},
debounceTimeoutMs: 400,
});
ちなみに、translations
の search_label
は <form>
要素に付与される aria-label
の値として使われます。少し過剰に感じたので、別途 JS の処理で属性自体を取り除いているため、上記のコードでは空の値を指定しています。
検索対象・除外の指定
見出し「検索対象・除外の指定」Pagefind では、HTML に特定の data-*
(カスタムデータ属性)を使用することで、検索結果の細かな調整が可能です。
本サイトでは検索対象を明示する data-pagefind-body
と、検索からの除外を明示する data-pagefind-ignore
を使用しています。
検索対象(data-pagefind-body
)
見出し「検索対象(」data-pagefind-body
を付けると、その要素以下のコンテンツが検索対象となります。
もし、この属性の指定が 1 つでも見つかると、すべてのページ内でこの data-pagefind-body
以下が検索の対象となります。つまり、属性の指定がないページは検索対象外になります。
このサイトでは、ブログ記事の <article>
要素以下を検索対象として指定しています。
<main id="main" class="main">
<!-- 検索対象 -->
<article class="article" data-pagefind-body>
<!-- ... -->
</article>
</main>
検索除外(data-pagefind-ignore
)
見出し「検索除外(」検索対象(data-pagefind-body
)以下で、検索から除外したい要素がある場合には、data-pagefind-ignore
を指定します。
<main id="main" class="main">
<!-- 検索対象 -->
<article class="article" data-pagefind-body>
<h1 class="article-header-title">CSS セレクタのレシピ - :has() 擬似クラス編</h1>
<!-- 検索除外 -->
<nav class="toc" aria-label="目次" data-pagefind-ignore="all">
<h2 class="toc-hdg">Table of Contents</h2>
<!-- ... -->
</nav>
<!-- 検索対象 -->
<p>CSS の <code>:has()</code> 擬似クラスは、すでに多くのブラウザではサポートされていますが、これまでは Firefox が非対応のため、実案件で使用するには限定的でした。</p>
</article>
</main>
この data-pagefind-ignore
には index
か all
のいずれかの値が指定でき、デフォルトの値は index
です。
index
では、検索のインデックスの対象からは除外されますが、フィルタやメタデータ、タイトル、サムネイル画像の候補からは外れないため、これらも含めてすべて対象外にしたい場合には all
を指定します。
本サイトでは、ブログ記事内の目次やシリーズ、コードブロック、ライブデモといった、検索結果のノイズになりそうな要素に data-pagefind-ignore="all"
を指定しています。
スタイルの変更
見出し「スタイルの変更」Pagefind では以下のいずれかの方法でスタイルをカスタマイズすることも可能です。
- CSS カスタム変数の値を変更する
- オリジナルの CSS を適用する
CSS カスタム変数の値を変更する
見出し「CSS カスタム変数の値を変更する」まずは、pagefind-ui.css
を読み込んだまま、CSS のカスタム変数の値を変更して、テーマを調整する方法です。サイトのトーンにあわせてスタイルを微調整したい場合に適しています。
CSS カスタム変数の一覧は以下のページを参照してください。
オリジナルの CSS を適用する
見出し「オリジナルの CSS を適用する」pagefind-ui.css
を読み込まずに、オリジナルのスタイルを適用する方法です。本サイトではこの方法で実装しています。
ただ、この方法にはリスクがあり、今後のバージョンアップ次第では UI のマークアップが変わる可能性があるため、そのタイミングで CSS を書き換える必要が出てくるかもしれません。
Astro の開発モードで Pagefind を読み込む
見出し「Astro の開発モードで Pagefind を読み込む」前述の導入方法では、ビルド後のファイルに対してプレビューをおこなっていました。
ただ、オリジナルの CSS を適用するといったように、UI の見た目を大きく変えたいときに、都度ビルドしてからプレビューするのは不便に感じます。
Astro の場合には、環境変数 import.meta.env.DEV
で、開発モードかを判別できるので、これを使用して JS ファイルの読み込みを振り分けます。
---
let jsPath = '/pagefind/pagefind-ui.js';
// 開発モードでは `/dist` からパスを指定する
if (import.meta.env.DEV) {
jsPath = '/dist' + jsPath;
}
---
<script src={jsPath}></script>
このように、少し強引ではありますが、開発モードではパスに dist
を含めて読み込んでいます。これで、開発モードでも Pagefind が使えるようになります。
Pagefind の問題点
見出し「Pagefind の問題点」Pagefind は、今のところ通常の使用方法であれば大きな問題はなく、必要十分な機能を備えていますが、以下の問題を認識しています。
先頭の文字がマッチする
見出し「先頭の文字がマッチする」例えば、現時点ではブログ記事で WebGL については言及していないので、結果はゼロになるはずですが、「WebGL」で検索すると先頭の「Web」の文字がマッチしてしまいます。
![「WebGL」で検索したときのスクリーンショット](/_astro/img-mismatch-01.B9x8QcKg_1E9Mur.webp)
まったく関連のない「二十四節気」と検索しても「二」の文字がマッチしてしまいます。
![「二十四節気」で検索したときのスクリーンショット](/_astro/img-mismatch-02.SmXSVITK_Z18U7l7.webp)
このように、マッチングの精度には改善の余地がありそうです。
なお、この記事もインデックス対象となるため、記事公開後は「WebGL」や「二十四節気」での検索結果は変わります。
日本語での検索
見出し「日本語での検索」Pagefind では、単語を空白で区切らない日本語や中国語でも単語の分割(Segmentation)には対応していますが、単語の語幹に一致させるステミング(Stemming)には非対応のようです。
また、検索する際には単語ごとに空白で区切る必要があるようです。
ただ、本サイトでは、高精度な検索機能を求めていないというのもありますが、現時点では日本語を使用した検索の使い勝手が悪いといった印象はありません。
絵文字のバグ
見出し「絵文字のバグ」インデックス内に存在しない絵文字を入力すると JS エラーが発生し、検索結果が表示されないバグがあるようです。これは公式サイトで試しても同様のようです。
例えば、本サイトの記事には絵文字「🚀(U+1F680)」が含まれているので検索しても問題ありませんが、公式サイトではエラーが発生して「Searching for 🚀…」で止まってしまいます。
![](/_astro/img-emoji.D-j9gRs__BOjIx.webp)
コンソールには以下のエラーメッセージが表示されます。
Uncaught (in promise) RuntimeError: unreachable
ただ、すべての絵文字が対象ではなく、「⭐(U+2B50)」のようにエラーが発生せずに「No results for ⭐」と正しく検索結果が表示される絵文字もあるようです。
おそらく絵文字のコードポイントが関係していそうですが、こちらも通常の使用で問題になるとは考えられないので、これ以上は深入りせずに許容範囲とします。
おわりに
見出し「おわりに」この記事では、本サイトのブログ記事検索における、Pagefind の導入方法を説明しました。
アカウント登録を必要とせず、HTML ファイルさえあれば軽量で使いやすいサイト内検索の機能が実装できるのは魅力的です。
ほかにもフィルタ機能や重み付け(ウェイト)の指定、サムネイル画像の指定、ソートの指定などさまざまなカスタマイズが可能です。JS 経由で API にアクセスすることもできるので、より高度な使い方もできます。詳しくは公式サイトを確認してみてください。
本サイトでもひとまず試験的に導入しているので、様子を見ながら、また少しずつチューニングしていければと考えています。