目次
静的サイトに高速で軽量な検索機能を追加したい場合、Pagefindは最適な選択肢の一つです。この記事では、Next.jsサイトにPagefind検索機能を実装する手順と、実装時に遭遇する問題の解決方法について詳しく解説します。
Pagefindとは
Pagefindは、静的サイト向けの全文検索エンジンです。サーバーサイドの処理が不要で、ビルド時に検索インデックスを生成し、フロントエンドのJavaScriptで高速な検索を実現します。
主な特徴:
- 軽量: 検索インデックスサイズが小さい
- 高速: クライアントサイドで瞬時に検索結果を表示
- 静的: サーバーサイドの処理が不要
- 多言語対応: 日本語を含む多言語の検索に対応
前提条件
- Next.jsプロジェクトがセットアップされていること
- 静的サイト生成(SSG)でサイトをビルドしていること
インストールと基本セットアップ
1. Pagefindのインストール
npm install -D pagefind
2. package.jsonにビルドスクリプトを追加
{
"scripts": {
"build": "next build && next export && npx pagefind --source out"
}
}
3. 検索対象ページにdata-pagefind-bodyを追加
検索対象にしたいページのメインコンテンツ部分にdata-pagefind-body
属性を追加します:
export default function BlogPost({ content }) {
return (
<article data-pagefind-body>
<h1>{content.title}</h1>
<div>{content.body}</div>
</article>
)
}
検索コンポーネントの実装
基本的な検索コンポーネント
import { useState, useEffect, useRef } from 'react'
declare global {
interface Window {
pagefind?: any
searchTimeout?: NodeJS.Timeout
}
}
export const SearchComponent = () => {
const [isSearchOpen, setIsSearchOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState<any[]>([])
const [isLoading, setIsLoading] = useState(false)
const searchRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const loadPagefind = async () => {
try {
if (!document.getElementById('pagefind-module')) {
const script = document.createElement('script')
script.id = 'pagefind-module'
script.type = 'module'
script.innerHTML = `
import * as pagefind from '/pagefind/pagefind.js';
window.pagefind = pagefind;
await pagefind.init();
`
document.head.appendChild(script)
}
} catch (error) {
console.warn('Pagefind loading failed:', error)
}
}
loadPagefind()
}, [])
const handleSearch = async (query: string) => {
if (!query.trim()) {
setSearchResults([])
return
}
if (!window.pagefind) {
console.warn('Pagefind not loaded yet')
return
}
setIsLoading(true)
try {
const results = await window.pagefind.search(query)
const processedResults = await Promise.all(
results.results.slice(0, 10).map(async (result: any) => {
const data = await result.data()
let url = data.url || data.raw_url || ''
// .html拡張子を除去してNext.jsルートに対応
if (url.endsWith('.html')) {
url = url.slice(0, -5)
}
return {
url,
title: data.meta?.title || data.title || 'Untitled',
excerpt: data.excerpt || '',
}
})
)
setSearchResults(processedResults)
} catch (error) {
console.error('Search error:', error)
setSearchResults([])
}
setIsLoading(false)
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const query = e.target.value
setSearchQuery(query)
clearTimeout(window.searchTimeout)
window.searchTimeout = setTimeout(() => {
if (query.length > 0) {
handleSearch(query)
} else {
setSearchResults([])
}
}, 300)
}
return (
<div className="w-full max-w-xl mx-auto mb-8 relative" ref={searchRef}>
<input
type="text"
value={searchQuery}
onChange={handleInputChange}
onFocus={() => setIsSearchOpen(true)}
placeholder="記事を検索..."
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{isSearchOpen && searchResults.length > 0 && (
<div className="absolute z-50 w-full mt-2 bg-white border border-gray-200 rounded-lg shadow-lg">
{searchResults.map((result, index) => (
<a
key={index}
href={result.url}
className="block p-4 hover:bg-gray-50 border-b last:border-b-0"
onClick={() => setIsSearchOpen(false)}
>
<h3 className="font-semibold text-gray-900 mb-1">
{result.title}
</h3>
<div
className="text-sm text-gray-600"
dangerouslySetInnerHTML={{ __html: result.excerpt }}
/>
</a>
))}
</div>
)}
</div>
)
}
よくある問題と解決方法
1. "Cannot use 'import.meta' outside a module"エラー
問題: PagefindはES Modulesを使用しているため、通常のscriptタグで読み込むとエラーが発生します。
解決方法: scriptタグにtype="module"
を指定してES Moduleとして読み込みます:
const script = document.createElement('script')
script.type = 'module' // 重要: この行を追加
script.innerHTML = `
import * as pagefind from '/pagefind/pagefind.js';
window.pagefind = pagefind;
await pagefind.init();
`
2. 検索結果のURLが404エラーになる
問題: PagefindがHTMLファイルのパス(例:/posts/article.html
)を返すが、Next.jsでは拡張子なしのパス(/posts/article
)でアクセスする必要があります。
解決方法: 検索結果処理時に.html
拡張子を除去します:
let url = data.url || data.raw_url || ''
if (url.endsWith('.html')) {
url = url.slice(0, -5) // .htmlを除去
}
3. 日本語入力時の検索タイミング問題
問題: 日本語のIME入力中に検索が実行されてしまい、意図しない検索結果が表示されます。
解決方法: IMEのcompositionイベントを監視して、確定後に検索を実行します:
const [isComposing, setIsComposing] = useState(false)
const handleCompositionStart = () => {
setIsComposing(true)
}
const handleCompositionEnd = (e) => {
setIsComposing(false)
const query = e.target.value
if (query.length > 0) {
handleSearch(query)
}
}
// input要素に追加
<input
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
// ...その他のprops
/>
検索結果のハイライト表示
Pagefindは検索キーワードを<mark>
タグでハイライトしてくれます。CSSでスタイルを設定しましょう:
mark {
background-color: #fef08a;
color: #1f2937;
font-weight: 600;
padding: 0 2px;
border-radius: 2px;
}
Tailwind CSSを使用している場合:
className="[&_mark]:bg-yellow-200 [&_mark]:text-gray-900 [&_mark]:font-semibold"
パフォーマンス最適化
検索結果の制限
大量の検索結果がある場合は、表示数を制限します:
results.results.slice(0, 10) // 最初の10件のみ表示
デバウンス処理
ユーザーの入力に対して即座に検索を実行せず、一定時間待ってから検索を実行します:
clearTimeout(window.searchTimeout)
window.searchTimeout = setTimeout(() => {
if (query.length > 0) {
handleSearch(query)
}
}, 300) // 300ms待機
まとめ
この記事では、Next.jsサイトにPagefind検索機能を実装する方法を詳しく解説しました。Pagefindを使用することで、サーバーレスで高速な検索機能を簡単に追加できます。
実装時のポイント:
- ES Moduleとしての正しい読み込み
- Next.jsルートに対応したURL正規化
- 日本語IME対応の実装
- 適切なデバウンス処理
これらの対策を行うことで、ユーザーフレンドリーな検索体験を提供できるでしょう。