如何为静态Next.js站点创建客户端搜索索引(无需CMS及第三方服务)
在Next.js中实现无第三方服务的客户端搜索
你的方案完全可行,而且是小体量内容场景下非常高效的选择——不需要依赖CMS或第三方搜索服务,完全在客户端完成搜索逻辑,下面是具体实现步骤和其他可选思路:
一、实现导出阶段生成搜索索引的方案
1. 自定义构建脚本挂钩导出流程
Next.js允许你通过修改package.json的构建命令,在next build完成后自动执行索引生成脚本。比如:
"scripts": { "build": "next build && npm run generate-search-index", "generate-search-index": "node scripts/generate-search-index.js" }
2. 编写索引生成脚本
创建scripts/generate-search-index.js脚本,核心逻辑是遍历你需要包含搜索内容的页面/文件,提取关键信息后生成JSON索引,并存放到public目录(客户端可直接访问)。
示例脚本逻辑(根据你的页面结构调整):
const fs = require('fs'); const path = require('path'); // 目标索引文件路径 const outputPath = path.join(__dirname, '../public/search-index.json'); // 需要扫描的页面目录(可替换为你的内容目录,比如content/) const pagesDir = path.join(__dirname, '../pages'); const searchIndex = []; // 遍历pages目录,排除系统页面(_app.js、_document.js等) fs.readdirSync(pagesDir).forEach(file => { if (file.startsWith('_') || !/\.(js|tsx|mdx)$/.test(file)) return; // 这里根据你的页面类型提取内容: // - 如果是MDX/Markdown文件:直接读取文件内容,解析标题、正文 // - 如果是React组件:假设组件导出了searchMetadata字段(包含标题、摘要等) const filePath = path.join(pagesDir, file); if (file.endsWith('.mdx')) { const content = fs.readFileSync(filePath, 'utf8'); // 简单解析MDX标题(实际可使用gray-matter等库) const titleMatch = content.match(/^#\s+(.*)$/m); const title = titleMatch ? titleMatch[1] : 'Untitled'; searchIndex.push({ url: `/${file.replace(/\.mdx$/, '')}`, title, content: content.replace(/^#.*$/m, '').trim().slice(0, 200) // 截取摘要 }); } else { const pageModule = require(filePath); if (pageModule.searchMetadata) { searchIndex.push({ url: `/${file.replace(/\.(js|tsx)$/, '')}`, ...pageModule.searchMetadata }); } } }); // 写入索引文件 fs.writeFileSync(outputPath, JSON.stringify(searchIndex, null, 2));
3. 客户端搜索实现
在React组件中加载/search-index.json,然后根据用户输入做匹配:
import { useState, useEffect } from 'react'; export default function SearchBar() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [searchIndex, setSearchIndex] = useState([]); // 加载索引文件 useEffect(() => { fetch('/search-index.json') .then(res => res.json()) .then(data => setSearchIndex(data)); }, []); // 实时过滤结果 useEffect(() => { if (!query) { setResults([]); return; } const lowerQuery = query.toLowerCase(); const filtered = searchIndex.filter(item => item.title.toLowerCase().includes(lowerQuery) || item.content.toLowerCase().includes(lowerQuery) ); setResults(filtered); }, [query, searchIndex]); return ( <div className="search-bar"> <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="搜索内容..." /> {results.length > 0 && ( <ul className="search-results"> {results.map(item => ( <li key={item.url}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </div> ); }
二、其他可行替代方案
直接内嵌搜索数据:如果内容极少(比如20页以内),可以直接在客户端组件里维护一个搜索数据数组,省去生成索引文件的步骤,比如:
const searchData = [ { url: '/about', title: '关于我们', content: '这是关于页面的内容...' }, // 其他页面数据 ];轻量级搜索库优化体验:如果需要模糊匹配、权重排序等功能,可使用
fuse.js(纯客户端库,体积小),替换简单的字符串匹配逻辑,提升搜索体验。ISR同步索引:如果内容需要增量更新(用Next.js ISR),可以在页面的
getStaticProps里更新索引文件,或者在ISR触发时重新生成索引,逻辑和静态导出类似,只是触发时机不同。
内容的提问来源于stack exchange,提问作者Mr. G
相关产品推荐
相关产品推荐

