You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为静态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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 06:35:19