Next.jsとPagefindで検索機能を簡単実装してみた

Next.jsとPagefindで検索機能を簡単実装してみた

Yuya Nishizawa

Yuya Nishizawa

こんにちは、新米エンジニアのにっしーこと西澤です。

今回、静的サイトに特化した検索ライブラリPagefindを使ってページ内検索機能を制作したので、使い方と実装方法についてご紹介します。

Pagefindとは?

Pagefindは、静的サイトの検索に特化した非常に汎用性の高いライブラリです。HTMLファイルがあるサイトであれば、基本的にどのようなサイトでも使用できます。そのためSSG(スタティックサイトジェネレーター)で開発されたサイトにも対応しています。

2023年9月に安定版がリリースされ、現在もアクティブ開発が続いているため、将来性も期待できます。

Pagefindで検索機能を実装してみた

▲今回作ったデモ

前提条件

開発環境
  • Next.js v14.2.3 Page Router
  • Typescript v5.4.5
  • microCMS
  • Pagefind v1.1.0

Pagefindの導入

自分で実装しようと思うとけっこう大変な検索機能が、たった4ステップで実装できます。

  1. ライブラリのインストール
  2. コマンドの設定
  3. コマンドを実行
  4. 検索機能の処理の作成

1. ライブラリのインストール

npm install pagefind

2. コマンドの設定

次にpackage.jsonにコマンドを設定します。Pagefindは静的に吐き出されるHTMLファイルを見にいくので、Next.jsのbuild後に実行されるように設定していきます。

既存のbuildコマンドを、以下のように変更してください。

"build": "next build && pagefind --site out --output-path 'public/pagefind/'",

3. コマンドを実行

npm run dev を実行する前にbuildをかけてください。

npm run build

buildコマンドを実行すると、Pagefindのファイルが–output-pathで指定したpublic/pagefind のフォルダに出力されます。

今回紹介した設定方法以外にもさまざまな設定方法があるので、自分に合ったものを探してみてください。

4. 検索機能の処理の作成

検索機能のコンポーネントファイルを作成し、以下の処理を記述します。

UiPagefind.tsx

import React, { useState, useEffect } from 'react';
import Link from 'next/link';

// styles
import styles from '@/styles/components/uiPagefind.module.scss';

// types
import { PagefindWindow, ResultType, ResultData } from '@/api/types';

// windowオブジェクトの型をPagefindWindowの型に当てはまるように指定
declare const window: PagefindWindow;

export default function SearchPage() {
  // 検索ワードを格納
  const [query, setQuery] = useState<string>('');
  // 検索結果を格納
  const [ results , setResults] = useState<ResultType[]>([]);

  // pagefindのファイルを読み込みにいく
  useEffect(() => {
    const loadPagefind = async () => {
      if (!window.pagefind) {
        try {
          window.pagefind = await import(
            '/pagefind/pagefind.js'
          );
        } catch (e) {
          window.pagefind = {
            search: async () => ({ results: [] as ResultType[] }),
          };
        }
      }
    };
    loadPagefind();
  }, []);

  // 検索結果を取得
  async function handleSearch() {
    if (window.pagefind) {
      const search = await window.pagefind.search(query);
      setResults(search.results);
    }
  }
  
  // 検索ワードが入力された際にhandleSearchを発火する
  useEffect(() => {
    handleSearch();
  }, [query]);

  // 
  return (
    <div>
	  // 検索バー
      <input
        type="text"
        placeholder="Search..."
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        className={styles.search}
      />
	  // 検索結果
      <div id="results" className={styles.result}>
        {results.length > 0 && (
          <h2 className={styles.subHeading}>検索結果...</h2>
        )}
        {results.map((result) => (
          <Result key={result.id} result={result} />
        ))}
      </div>
    </div>
  );
}

// 検索結果として出力するものを作成
function Result({ result }: { result: ResultType }) {
  const [data, setData] = useState<ResultData | null>(null);

  useEffect(() => {
    async function fetchData() {
      const resultData = await result.data();
      setData(resultData);
    }
    fetchData();
  }, [ result ]);

  if (!data) return null;

    // 検索結果として出したい要素
  return (
    <div className={styles.container}>
      <Link href={data.url}>
        <p dangerouslySetInnerHTML={{ __html: data.excerpt }} />
      </Link>
    </div>
  );
}

Types

type Anchor = {
  element: string;
  id: string;
  text: string;
  location: number;
};

type ResultData = {
  anchors: Anchor[];
  content: string;
  excerpt: string;
  filters: Record<string, unknown>;
  locations: number[];
  meta: {
    title: string;
  };
  raw_content: string;
  raw_url: string;
  sub_results: unknown[];
  url: string;
  weighted_locations: unknown[];
  word_count: number;
};

type ResultType = {
  id: string;
  data: () => Promise<ResultData>;
};

export interface PagefindWindow extends Window {
  pagefind?: {
    search: (query: string) => Promise<{ results: ResultType[] }>;
  };
}

使い方

コンポーネントを反映させていきます。

反映前


 

import SearchPage from '@/components/UiPageFind';

検索機能のコンポーネントをインポートします。

 

<SearchPage />

検索機能を導入したい部分に上記タグを入れます。

反映後

反映されると赤枠のように検索バーが追加されます。検索バーにキーワードを入力すると、そのキーワードを含む関連記事が表示されます。

ここまでできれば完成です。

おまけ:CSSの適応

デモのcss
※cssはお好きなstyleに変更してください。

uiPagefind.module.scss

.search {
  margin: 30px auto;
  padding: 20px 10px;
  width: 100%;
  border: 2px solid #000;
  border-radius: 4px;
}

.subHeading {
  margin-bottom: 10px;
  color: #020202;
  opacity: 0.6;
  font-size: 20px;
  font-weight: 700;
}

.container {
  padding: 15px;
  box-sizing: border-box;
  border: 2px solid #000;
  border-bottom: none;
  &:last-child {
    border-bottom: 2px solid #000;
  }
}

開発時に発生したエラーとその対処法

①ESLintとPagefindの互換性

問題 ESlintを導入していたため、Pagefindの呼び出し時にESlintのエラーが発生してしまった。
対処法 Pagefindの記述方法がESLintの規則に合致しないことが原因です。ESlintのignore設定を追記しましょう。

// @ts-expect-error pagefind.js generated after build
/* webpackIgnore: true */

②Pagefindの相対パスによるルーティング障害

問題 Pagefindから返されるレスポンスのURLを相対パスとして直接使用したところ、正しくルーティングされない。
対処法 レスポンスのURLのデータをもとに自分でURLを新しく作り直し、それを使用しましょう。

おわりに

今回Pagefindを使ってページ内検索を制作しましたが、Next.jsで使用されている記事が少なくとても苦戦しました。きっと今後アップデートでNext.jsでもっと簡単に導入できるようにしてくれると願ってます。

とはいえスクラッチで検索機能を開発するのに比べたら、何十倍も楽にハイクオリティなものを制作できるので、みなさんぜひお試しください。

またPagefindは今回使用した機能の他に検索結果にフィルターをかけたり特定の要素を検索結果から外せる等色々な設定ができるのでそちらもお試しください。

参考記事

Add search to your Next.js static site with Pagefind

この記事のシェア数

フロントエンドエンジニアとして主に大手メーカーのWebサイト制作・保守業務を担当。JS、React、Nextのフレームワークに関しての深い知見を持つ。中学時代からIT分野に興味を持ち、高校でWebプログラミングの基礎を学ぶ。専門学校卒業を経て2024年に新卒入社。

このメンバーの記事をもっと読む
10年以上の開発実績があるLIGが、最適な開発体制や見積もりをご提案します
相談する サービス概要を見る