【Next.jsで使用】脳死でサイト内検索を実装する為にPagefindを使ってみたので簡単に紹介【SSG 静的サイト向け】
個人で運用している静的サイト(Next.js、SSGで利用)としてホスティングしているサービスにサイト内検索を特に難しいことを考えることなく実装できないかと考えていた時、Pagefindという全文検索ライブラリを見つけました。今回は本番環境にこのPagefindを入れてみた感想やHowTo的な話をしていきたいと思います。
Pagefindとは?
Pageindは、大規模なサイトでもパフォーマンスをいい感じに、帯域幅をできる限り使用しないで実現する静的検索ライブラリで、HugoやNext.js、Astro・SvelteKitなどで使用することができます。(詳しくはHPをみてください)
投資できるお金の少ない個人開発者や中小企業がオンラインにWebサービスを公開する場合にはどうしても帯域幅の懸念をしなくてはなりません。
例えばVercelであれば、Proプランで帯域幅1TBまでをサポートしてくれていますが、それ以上になるとちょっとコスパは考えもの等 結構な支出にもなるので、Pagefindの帯域幅をできる限り使用しないコンセプトは魅力の1つと言えるでしょう。
環境
- Next.js(v14.0.4)
- pages router
- SSGのみで使用
- Typescript環境
導入(環境)
- 普通に入れます
npm i pagefind
- package.jsonのscriptにコマンドを追加します
(例)
"pagefind": "pagefind --site .next --output-path public/pagefind"
- ファイルを出力したい場合には、先にbuildしておく必要があります
npm run build
- 普通にpagefindコマンドを打ちます
npm run pagefind
すると、 public/pagefind
以下に検索に用いる静的ファイル等が出力されます。
2~4のプロセスは自分で好きにカスタマイズしていいです。
自分はpagefindを入れたプロジェクトでは、buildとpostbuildの中にpagefindやサイトマップ生成その他色々やってたりします。
導入(実用)
以下のようにページを作りましょう。inputに文字を入れると、いい感じに取得してきます。
import { PageFindData } from '@/types/PageFindData';
import { PageFindResult } from '@/types/PageFindResult';
import React, { useEffect, useState } from 'react';
export default function SearchPage() {
const [query, setQuery] = React.useState('');
const [results, setResults] = React.useState<PageFindResult[]>([]);
async function loadPagefind() {
if (typeof window.pagefind === 'undefined') {
try {
// pagefindの読み出しを行う
const pf = await import(
// @ts-expect-error pagefind.js generated after build
/* webpackIgnore: true */ '/pagefind/pagefind.js'
);
window.pagefind = pf;
} catch (e) {
// todo: エラー処理
}
}
}
useEffect(() => {
loadPagefind();
}, []);
async function searchQuery() {
if (window.pagefind) {
try {
const search = await window.pagefind.search(query);
console.log(search);
setResults(search.results);
} catch (e) {
console.log(e);
}
}
}
return (
<div>
<input
className="bg-red-50 h-12 w-full my-3"
type="text"
placeholder="検索ワード入れる..."
value={query}
onChange={e => setQuery(e.target.value)}
onInput={searchQuery}
/>
<div id="results">
{results.map(result => (
<ResultItem key={result.id} result={result} />
))}
</div>
</div>
);
}
const ResultItem = ({ result }: { result: PageFindResult }) => {
const [data, setData] = useState<PageFindData>();
useEffect(() => {
async function fetchData() {
const data = await result.data();
setData(data);
}
fetchData();
}, [result]);
return data ? (
<div>
<p>{data.url}</p>
<p>{data.content}</p>
</div>
) : null;
};
ちなみに上記のResultItemで表示しているデータの中は
interface PageFindData {
url: string;
content: string;
word_count: number;
meta: Meta;
anchors: Anchor[];
weighted_locations: Weightedlocation[];
locations: number[];
raw_content: string;
raw_url: string;
excerpt: string;
sub_results: Subresult[];
}
のようなデータです。PageFindResultの中は候補スコアが含まれていたりします。
ただし、urlやcontentは都合の良いデータではないので良い感じに整形してあげる必要があります。
実際に使ってみて
30分もかからずにサイト内検索のロジックを実装することができました。Pagefind様様です。
ステルスでやっているサービスで採用したので、サービスをお見せすることはできないのですが、そこそこ満足なサイト内検索ができます。上記でもお話ししたように、都合良いデータとして取得できるわけではないところがUX的な課題だなと感じました。
自分は総合的なパフォーマンスには満足しています。比較的予算がない場合にやる策としては結構ありかもしれません。
参考にしたサイト
Discussion