ネットソリューション

【Pagefind】静的HTMLで検索機能を追加する

本メディアを立ち上げてから、早くも5か月が経ち、公開した記事も20本に達しました。しかし、ブログ形式の宿命として、古い記事がどんどん埋もれてしまう問題があります。そこで今回は、過去の記事にもアクセスしやすくするために、検索機能を実装してみようと思います。

本メディアはAstroを使用し、最終的には静的なHTMLとして出力しています。
そのため、通常であれば検索機能には何らかの外部API(サービス)が必要になります。
しかし、静的サイト向けの検索ライブラリ「Pagefind」を使えば、APIなしでも実装できるらしいと知り、試してみることにしました。

Pagefind

Pagefind

Pagefindは完全に静的な検索ライブラリで、大規模サイトでもユーザーの帯域幅を最小限に抑え、サーバーインフラ不要で利用できることを目的としています。
PagefindはHugo、Eleventy、Jekyll、Next、Astro、SvelteKitなど、あらゆるサイトフレームワークに対応しており、ビルドされた静的ファイルを含むフォルダを指定するだけで、ほぼ設定不要で始められます。
インデックス生成後、Pagefindは静的な検索バンドルを出力ファイルに追加し、JavaScriptによる検索APIを提供します。また、設定不要で利用できるUIも付属しています。
Pagefindの目標は、数万ページ規模のウェブサイトでも、低帯域でのブラウザ内検索を実現することです。検索インデックスは小さなチャンクに分割されており、必要な部分のみをロードします。例えば、1万ページのサイトで300kB以下、一般的には100kB程度のネットワーク負荷でフルテキスト検索が可能です。
(ChatGPTによる翻訳&要約)

実装参考

Add Search To Your Astro Static Site(https://blog.otterlord.dev/posts/astro-search/)

こちらのページを参考に作業を進めていきます。

Pagefindのインストール

Pagefindをインストールするには、次のコマンドを使用します。

npm install --save-dev pagefind

postbuildにpagefindの実行を追加

Astroのビルド後に自動でPagefindを実行するために、package.jsonに以下の設定を追加します。

package.json
"postbuild": "pagefind --site dist/techblog"

Pagefindは、出力されたHTMLファイルを解析し、検索に必要なデータを生成します。このため、Astroの出力フォルダであるdistを対象として指定します。今回はtechblogがルートディレクトリになっているため、dist/techblog を指定しました。

これでビルド後、HTMLを基に検索データがdist/techblog/pagefindフォルダ内に生成されます。このデータを使用して、静的な検索機能が実装可能になります。

検索除外設定

HTMLタグにdata-pagefind-ignoreを追加することでそのタグを検索対象から除外することができます。
ヘッダーフッターなどの共通要素や、一覧ページなどに適時追加しておきましょう。

機能確認

Pagefindの検索機能が正常に動作するかを確認するため、参考サイトより以下のHTMLコードをページに埋め込みます。
検索ボックス(input)と検索結果の表示エリア(#results)を含んでおり、検索語句の入力に応じて検索結果がリアルタイムに表示される仕組みです。

<input id="search" type="text" placeholder="Search..." />

<div id="results"></div>

<script is:inline>
  document.querySelector("#search")?.addEventListener("input", async (e) => {
    // Pagefindのスクリプトを最初の検索時にのみ読み込み
    if (e.target.dataset.loaded !== "true") {
      e.target.dataset.loaded = "true";
      // Pagefindのスクリプトを読み込む
      window.pagefind = await import("/techblog/pagefind/pagefind.js");
    }

    // 入力された値でインデックスを検索
    const search = await window.pagefind.search(e.target.value);

    // 以前の検索結果をクリア
    document.querySelector("#results").innerHTML = "";

    // 新しい検索結果を追加
    for (const result of search.results) {
      const data = await result.data();
      document.querySelector("#results").innerHTML += `
    <a href="${data.url}">
      <h3>${data.meta.title}</h3>
      <p>${data.excerpt}</p>
    </a>`;
    }
  });
</script>

確認のポイント

Astroのビルドプロセスの完了前には、Pagefindによって生成されるJavaScriptファイルが存在しません。そのため、検索機能の動作確認はビルド後のファイルで行います。

ビルド後、ブラウザで確認すると、このように検索ボックスに入力したキーワードに対応するページが表示されました!

リアルタイムで検索結果が反映されている

このHTMLに、CSSを当てて見た目を整えれば最低限の実装は完了ですね!

記事一覧の見た目に変更する

以前の記事では、記事一覧のJSON データを出力する方法を紹介しました。
Pagefindの検索結果と、このJSONデータを組み合わせて、記事一覧のような見た目に変更してみます。

記事一覧のJSONデータは次のような形式です。

JSON例
[{
    "title": "Astroから記事一覧のjsonを出力する Astroを使ってオウンドメディアを作る-5",
    "description": "Astroから全記事のJSONデータを作成し、JSONから記事一覧を出力する方法を解説します。",
    "slug": "net-solution/2024/08/astro-blog-5",
    "pubDate": "2024-08-23T00:00:00.000Z",
    "ogImage": "ogp-astro-blog-5.png",
    "author": "山下",
    "tags": [
        "Astro",
        "オウンドメディア",
        "JSON"
    ]
}]

検索結果と JSON データを組み合わせる

Pagefindの検索機能で返ってきた結果に、all-posts.jsonから取得した記事データをマッチさせ、一覧記事の見た目で出力するように変更しました。

  // JSON データをロードする非同期関数
  let jsonData = null;
  async function loadJson() {
    const response = await fetch("/techblog/api/all-posts.json");
    jsonData = await response.json();
  }
  loadJson();
  // Pagefind の検索結果を使って、記事一覧を表示
  for (const result of search.results) {
    const data = await result.data();
    const article = jsonData.find(item => data.url.includes(item.slug));
    document.querySelector("#results").innerHTML += createArticle(article);
  }
  • loadJson 関数で /techblog/api/all-posts.json をフェッチし、jsonData に格納しておきます。
  • search.results 内の各検索結果に対して、jsonDataのslugを使って該当記事を検索し、マッチしたデータで記事要素を生成します。
関連記事出力のメソッドにつなぐことで簡単に見た目を再現できました。

おまけ:入力候補付きの検索の作成

検索欄に入力候補が表示されるようにlist属性を使って、検索時の補助機能を追加できます。
これにより、ユーザーが入力途中で候補を選択でき、素早く検索できるようになります。

以下のように、inputタグにlist属性を追加し、それに対応するdatalistを用意します。

<input list="list" id="search" type="text" placeholder="Search..." />
<datalist id="list">
  <option value="アクセシビリティ"></option>
  <option value="キャラクター"></option>
  <option value="コラム"></option>
  <option value="スクリーンリーダー"></option>
  <option value="デザイン"></option>
  <option value="レタッチ"></option>
  <option value="Astro"></option>
  <option value="Photoshop"></option>
  <option value="WordPress"></option>
</datalist>

Webの進化の速さにアップアップですがなんとか喰らいついていきたい。

山下 X@Frencel_ns

フロントエンドエンジニア
フレームワーク頑張りたい人
モンハンワイルズではキアヌ・リーブス似のイケオジでプレイしたい

好きなモンスターハンターの武器

双剣・狩猟笛

mail お問い合わせ

ご覧いただきありがとうございます。
当メディアへのご質問や各事業部へのお仕事のご相談がありましたら、お気軽にお問い合わせください。

article 過去記事