pagefindを利用したブログ検索機能を実装する

🌐

この記事は DeepL によって翻訳されました。誤訳があれば教えてください!

Pagefindは静的なウェブサイト用に設計されたクライアントサイドの検索ライブラリです。静的サイトジェネレータ(SSG)で作ったウェブサイトやNext.js、Gatsby、Hugo、Jekyllなどのフレームワークで構築されたサイトに強力な検索機能を追加することができます。外部APIやサービスキーなしでローカルでインストールと設定が可能なので、とても簡単かつ迅速に適用することができます。

動作方法

Pagefindは次のようなプロセスで動作します。

  1. インデックス作成段階:サイト構築後、HTMLファイルを分析して検索インデックスを作成します。この段階では、テキストコンテンツ、タイトル、メタデータなどが抽出され、処理され、インデックスされたデータを利用できるようにするjsファイルが生成されます。つまり、ビルド時にhtmlを作成した場合のみ使用できることを意味します。
  2. 検索API: インデックス生成後、提供されたJavaScript APIを使用してサイトに検索インターフェースを実装することができます。
  3. UI生成: ユーザーが検索語を入力すると、Pagefindはあらかじめ生成されたインデックスを使用して関連ページとセクションを素早く見つけて結果を提供し、私たちはこの結果を利用してUIを実装するだけです。

様々な機能と使い方はDocsに記載されています。

Getting Started with Pagefind | Pagefind - Static low-bandwidth search at scale.

クイックスタート!

script作成

さっそく適用してみましょう。 どのようなパッケージをインストールする必要がなく、下記の内容に従ってください。開発環境はNext.js 15と pnpmを使っています。

まず、package.jsonに postbuildを追加します。

"scripts":{ // ...
  // ...
  "postbuild":"npx pagefind --site .next --output-path public/pagefind"、
  // ...
} を指定します、

**インデックスされるためにはhtmlを生成する必要があり、htmlが生成されるためにはnextがビルドされる必要があることを覚えておきましょう。**このスクリプトを追加して、ビルドしてみるとpublicフォルダの下にpagefind関連のファイルがあることが確認できます。

Image.png

ここでできたpagefind.jsだけimportして使うことができます。

API呼び出し

まず、全体のコードは下記のようになります。

export default function Search() { { const [search, setSearch] = useState("")
  const [search, setSearch] = useState("");
  const [results, setResults] = useState<PagefindResult[]>([]); const [results, setResults] = useState<PagefindResult[]>([]);
  const [pagefind, setPagefind] = useState<any>(null);

  useEffect(() => { {
    const initPagefind = async () => { {
      try { // 実行時に動的に読み込みを試みる
        // ランタイムに動的に読み込みを試みます。

        setPagefind(
          await import(
            // @ts-expect-error
            "./pagefind/pagefind/pagefind.js"
          )を呼び出します、
        )です;
      } catch (error) {
        console.error("Pagefind 初期化失敗:", error);
      }
    };

    // クライアント側でのみ実行
    if (typeof window !== "undefined") {
      initPagefind();
    }
  }, []);

  const handleSearch = async (e: any) => { {
    setSearch(e.target.value);
    if (!pagefind || e.target.value === "") { {
      setResults([]);
      を返します;
    }

    const search = await pagefind.search(e.target.value);
    const results = await Promise.all(search.results.map((r) => r.data()));
    console.log(results);
    setResults(results);
  };

  return (
    <div><input
        type="text"
        value={search}
        onChange={handleSearch}。
        placeholder="検索語を入力してください..."
      /> ←このように設定します。

      <div> </div
        {results.map((result, i) => (
          <div key={result.url}> <div key={result.url}> </div
            <a href={result.url}>{result.meta.title}</a></div> </div
        ))}
      </div> </div> </div
    </div> </div> </div
  )です;
}

UI生成

呼び出しをしたので、結果を見てみましょう。

Image.png

主に確認できる項目は下記のようになります。

  • excerpt: コンテンツ内でキーワードを持ってる部分を一部パースしたものです。
  • **meta:**コンテンツ内でmeta情報を取得します。 titleの場合は一番最初に出会うh1タグ、画像はh1タグの後に一番最初に出会うimageタグです。

全て追加オプションなどを使ってカスタムでインポートすることができるので、Docsをよく見てみましょう!

問題発生

私のブログは韓国語、英語、中国語、日本語の4つの言語を提供していますが、検索すると全ての言語の記事が表示されていました。

Image.png

このような多言語ページは一般的なケースなので、当然pagefindでもMultilingual search機能を提供しています。しかし、これを判断する基準がhtmlタグのlang属性にどんな値があるかで判断していました。

しかし、Next.jsではLayoutでその設定をしなければならないのですが、私が探してみた時、静的ページビルド時にlangの値を取得する方法がありませんでした。(あったら教えてください)

解決方法

それで、迂回的な方法でpagefindのfilter機能を使って解決しました。各ページのh1タグに下記のように追加属性を付与しました。

<h1
  data-pagefind-filter="lang[data-lang]"
  data-lang={params.lang}。
  className="text-3xl md:text-5xl font-bold"
>
  {title}
</h1> </h1

そして、APIリクエストをする時、filtersを追加します。

const search = await pagefind.search(e.target.value, {
  filters: { {
    lang:"ko"、
  }、
});

終わりに

小さくて軽いライブラリpagefindを使って検索機能を早く実装してみました。外部サービスに依存せず、独自に検索インデックスを生成するのでプライバシーとコントロール面でもメリットがあり、また、ユーザーエクスペリエンス面でも速い応答速度を提供するので、ブログやドキュメントサイトを運営する場合、とても便利だと思います。