makenowjust-labs/blog

MakeNowJust Laboratory Tech Blog

ブログにPagefindを導入しました

2024-01-18

Pagefindとは静的サイトで動作する (バックエンドのサーバーを必要としない) 全文検索エンジンです。

このブログにPagefindによる全文検索機能を導入しました。 右上の虫眼鏡ボタンをクリックすると、検索用のダイアログが開くはずです。

この記事では、どのようにしてNext.js製のサイトにPagefindを導入したのかを説明します。

はじめに

Pagefindは静的サイトで動作する全文検索エンジンです。 「静的サイトで動作する」というのはバックエンドにサーバーなどを必要とせず、静的にビルドされたファイルのみで検索が実現できることを意味します。

つまり、このブログのようにGitHub Pagesで公開されているサイトであっても、Pagefindを使うことで検索機能が実現できます。 今回、Pagefindを使ってこのブログに検索機能を追加しました。 ナビゲーションバーの右側に虫眼鏡ボタンがあるはずです。 そちらをクリック (タップ) すると、検索用のダイアログが表示されるはずです。

検索用のダイアログが表示されている様子

検索用のダイアログが表示されている様子

ちなみに ⌘+K とか / でも検索用のダイアログを表示できます。

最近ではAstro製のドキュメントサイトビルダーであるStarlightの検索エンジンとして、Pagefindが採用されています。

Pagefindは自身のサイトで次のように述べています。

Pagefind runs after Hugo, Eleventy, Jekyll, Next, Astro, SvelteKit, or any other website framework.

しかし、実際に導入してみると、いくつか苦労するところがありました。 これから、それらや解決策を説明していきたいと思います。

通常のPagefindの導入の仕方

PagefindのQuick Startでは、次の2段階で導入すると説明しています。

  1. 対象のサイトにPagefindを読み込むコードを追加する。

    <link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
    <script src="/pagefind/pagefind-ui.js"></script>
    <div id="search"></div>
    <script>
      window.addEventListener("DOMContentLoaded", (event) => {
        new PagefindUI({ element: "#search", showSubResults: true });
      });
    </script>
  2. ビルドされたサイトに対して pagefind コマンドを実行して、インデックスや関連ファイル (上の pagefind-ui.csspagefind-ui.js) を作成する。

    npx -y pagefind --site public --serve

この方法は通常の静的サイトであれば (恐らく) 上手く動作します。 しかし、この方法では次のような問題が考えられます。

  • 開発中に検索が利用できず、さらにエラーが起こる
  • トップページなどを含めたすべてのページがインデックスされてしまう

これらの問題は追加するコードを工夫したり、インデックスの作成のコマンドに設定を追加すればある程度は改善できるはずです。 ですが、今回は別のアプローチで問題を解決することにしました。

Node.js APIを使ったPagefindインデックスの作成

Pagefindはコマンドから作成する他に、Node.jsのAPIでインデックスを作成することもできます (ドキュメント)。 このブログでは、こちらを使ってインデックスを作成することにしました。

インデックスを作成するためには、まずcreateIndex関数を使ってindexオブジェクトを生成します。

import { createIndex } from "pagefind";
 
const { index } = createIndex();

そして、このindexに対してaddCustomRecordメソッドを呼び出すことで、検索対象を追加していきます。

await index.addCustomRecord({
  url: "/post/2023-08-06-pike-earley",
  content:
    "正規表現マッチングの実装手法の1つとしてPike VMと呼ばれるものがあります。これは...",
});

そして、最後にwriteFilesを呼び出すことで、インデックスをファイルに書き出します。

await index.writeFiles({
  outputPath: "public/pagefind",
});

publicディレクトリ以下に出力していることに注目してください。 Next.jsではpublicディレクトリには、ルートディレクトリから配信される静的ファイルを配置します。 そのため、ビルド前にインデックスの生成を行い、開発時の参照が可能になります。

実際の生成はscripts/pagefind.jsで行っています。

PagefindUIのスクリプトのロード

インデックスが生成できたので、次はNext.jsからpagefind-ui.jspagefind-ui.cssを読み込んで、適用する部分を追加します。

これには、next/scriptScriptコンポーネントを利用しました。

const searchRef = createRef<HTMLDivElement>();
 
const setupSearchBox = useCallback(() => {
  new window.PagefindUI({
    element: searchRef.current,
  });
}, [searchRef]);
 
return (
  <>
    <Script
      strategy="lazyOnload"
      onReady={setupSearchBox}
      src={`/pagefind/pagefind-ui.js`}
      stylesheets={[`/pagefind/pagefind-ui.css`]}
    />
    <div ref={searchRef} />
  </>
);

実際のソースコードはcomponents/Pagefind.tsxにあります。 これに加えて、ダイアログの開閉の制御なども行っています。

また、このブログの場合はbasePath/blob/になっているため、そのままだとパスが異なってしまい、正しく読み込みができません。 そこで、next.config.jsenv経由でbasePathを渡し、参照するようにしています。

あとがき

そんなこんなでブログに検索機能を実装しました。 記事がある程度増えていて欲しいと思っていた機能だったので、この機会に追加できて良かったと思います。

本年もよろしくお願いします。