「Next.jsのoutput: 'export'で静的サイト作ってるけど、記事が増えてきたから全文検索つけたいんだよね。でも静的サイトだから検索APIとか用意するの面倒くさいな...」って思ったことない?
うちのIVYXONサイトも静的サイトなんだけど、これで全文検索を実現してるよ。ビルド時に検索用のJSONインデックスを自動で作っちゃえばいいんだ。
一番雑な投げ方
まずは、これだけClaude Codeに投げてみて。
Next.jsのoutput: 'export'で静的サイトを作ってる。
ビルド時に全記事のタイトルと内容から検索用JSONファイルを自動生成してほしい。
ファイル名は public/search-index.json にして。
これだけで動く。Claude Codeは、必要なスクリプトの提案から、package.jsonの更新までやってくれるよ。
もうちょい具体的に投げるパターン
もうちょい細かく指示したいなら、こんな感じで投げてみて。
1. 対象ファイルと抽出項目を具体的に指定
「どのファイルから、何を抽出するのか」を明確にすると、より正確なインデックスが作れる。
Next.jsのoutput: 'export'で静的サイトを作ってる。
インデックスの対象は app/posts/**/*.md 以下のMarkdownファイル。
Front Matterの title と、本文(HTMLタグは除外してプレーンテキストで)だけ抽出して。
生成するJSONファイルは public/search-index.json に。
2. JSONのデータ構造を指定
検索時に扱いやすいように、出力されるJSONのフォーマットも指定できる。
生成されるJSONは、[{ slug: '記事のスラッグ', title: '記事のタイトル', content: '記事の本文プレーンテキスト' }] の配列形式にしてほしい。
slugはファイル名から拡張子を除いたものを使って。
3. ビルドプロセスへの組み込みまで丸投げ
スクリプトを作るだけでなく、ビルドプロセスに組み込むところまで頼んじゃおう。
生成スクリプトは scripts/build-search-index.mjs として作ってほしい。
npm run build 時に自動で実行されるように package.json の build スクリプトも更新しといて。
実践例 / 実録
うちのIVYXONサイトでも、この仕組みで記事の全文検索を動かしてるんだ。実際にClaude Codeに投げたらどうなったか、実録形式で紹介するね。
俺はまず、雑に「静的サイトで検索インデックス作って」ってClaudeに投げた。するとClaudeは、こんなスクリプトを提案してきたよ。
scripts/build-search-index.mjs
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import strip from 'strip-markdown'; // remark-strip-markdown の後継
const POSTS_DIRECTORY = path.join(process.cwd(), 'app', 'posts');
const OUTPUT_FILE = path.join(process.cwd(), 'public', 'search-index.json');
async function getPostData(filePath) {
const fileContents = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContents);
const processedContent = await remark().use(strip).process(content);
const plainTextContent = String(processedContent);
const slug = path.basename(filePath, '.md');
return {
slug,
title: data.title || '',
content: plainTextContent,
};
}
async function buildSearchIndex() {
const postFiles = fs.readdirSync(POSTS_DIRECTORY).filter(file => file.endsWith('.md'));
const allPostsData = await Promise.all(
postFiles.map(async (fileName) => {
return await getPostData(path.join(POSTS_DIRECTORY, fileName));
})
);
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(allPostsData, null, 2));
console.log(`Search index built at ${OUTPUT_FILE}`);
}
buildSearchIndex();
これ、fsとpathでMarkdownファイルを読み込んで、gray-matterでFront Matterと本文を分けてる。ポイントはremark().use(strip).process(content)の部分。これでMarkdownをプレーンテキストに変換して、余計な記号とかHTMLタグを全部消してるんだ。
次に、このスクリプトをnpm run buildの前に実行するように、package.jsonも更新するよう提案してきた。
package.jsonのscripts部分
{
"name": "your-nextjs-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "node scripts/build-search-index.mjs && next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
// ...
"gray-matter": "^4.0.3",
"remark": "^15.0.1",
"strip-markdown": "^6.0.0"
},
"devDependencies": {
// ...
}
}
こうしておけば、npm run buildって打つだけで、Next.jsのビルドの前に検索インデックスが自動的に最新の状態になるってわけ。あとはクライアントサイドでこのJSONを読み込んで検索ロジックを組めば、静的サイトでも全文検索ができちゃうよ。
つまずきポイント
output: 'export'だとビルド時しかデータにアクセスできない- Next.jsの静的エクスポートを使ってるなら、記事の読み込みやインデックス生成はすべてビルド時に完結させる必要があるんだ。サーバーサイドレンダリング(SSR)やサーバーサイドデータフェッチングはできないから気を付けて。
- インデックスのテキストクリーニング
- Markdownからプレーンテキストに変換するとき、不要なHTMLタグやマークダウン記号が残ってると検索結果が汚くなりがち。
strip-markdownみたいなツールを使って、しっかりクリーンなテキストにすることが重要だよ。
- Markdownからプレーンテキストに変換するとき、不要なHTMLタグやマークダウン記号が残ってると検索結果が汚くなりがち。
- インデックスサイズとビルド時間
- 記事の数が増えると
search-index.jsonのファイルサイズも大きくなるし、インデックス生成にかかる時間も長くなることがある。もしサイトがものすごく大きくなったら、ビルドプロセスの見直しが必要になるかもしれないね。
- 記事の数が増えると