pagefindを使用したブログ検索機能の実装

profile image

静的ウェブサイト向けのクライアントサイド検索ライブラリpagefindを活用して、ブログに素早く簡単に検索機能を追加しました。

この記事は Jetbrains's Coding Agent Junie junie logoによって翻訳されました。誤訳があれば教えてください!

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

クイックスタート!

スクリプトの作成

早速適用してみましょう。パッケージをインストールする必要はなく、以下の内容に従うだけです。開発環境はNext.js 15pnpmを使用しています。

まず、package.jsonpostbuildを追加します。

json
"scripts": {
  // ...
  "postbuild": "npx pagefind --site .next --output-path public/pagefind",
  // ...
},

インデックス作成にはHTMLの生成が必要で、HTMLを生成するにはNextのビルドが必要であることに注意してください。 このスクリプトを追加してビルドすると、publicフォルダの下にpagefind関連ファイルが生成されるのが確認できます。

Image.png

あとはここで生成されたpagefind.jsをインポートして使用するだけです。

API呼び出し

まず、全体のコードは以下の通りです。

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

  useEffect(() => {
    const initPagefind = async () => {
      try {
        // ランタイムで動的にロードを試みる

        setPagefind(
          await import(
            // @ts-expect-error
            "./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([]);
      return;
    }

    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>
        {results.map((result, i) => (
          <div key={result.url}>
            <a href={result.url}>{result.meta.title}</a>
          </div>
        ))}
      </div>
    </div>
  );
}

UI生成

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

Image.png

主に確認できる項目は以下の通りです。

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

すべて追加オプションなどでカスタマイズして取得できるので、Docsをよく確認しましょう!

問題発生

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

Image.png

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

しかし、Next.jsではLayoutでその設定をする必要がありますが、私が調べた限りでは、静的ページビルド時にlang値を取得する方法がありませんでした...(もしあれば、ぜひ教えてください)

解決方法

そこで、回避策としてpagefindのfilter機能を利用して解決しました。各ページのh1タグに以下のように追加属性を付与しました。

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

そして、API要求を行う際にfiltersを追加します。

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

おわりに

小さく軽量なライブラリpagefindを通じて検索機能を素早く実装してみました。外部サービスに依存せず独自に検索インデックスを生成するため、プライバシーと制御の面でも利点があり、またユーザー体験の面でも素早い応答速度を提供するため、ブログやドキュメントサイトを運営する場合に非常に有用だと思います。

❤️ 0
🔥 0
😎 0
⭐️ 0
🆒 0