-
Step 1 · 选方案:构建期索引 / 自建倒排 / 第三方搜索
先按规模选路子,别一上来就接付费搜索
给静态站加搜索只有三条路,选错会白折腾一周。
| 方案 | 做法 | 适合 | 代价 |
|---|---|---|---|
| 构建期索引(Pagefind 一类) | 发布前扫一遍 HTML,生成索引文件,前端 fetch 查询 | 十几页到几千页的静态站 | 每次改内容都要重跑一次索引 |
| 自建倒排索引 | 用已有的 JSON 数据在前端建索引自己查 | 内容本来就由 JSON 驱动、页数少(几十页以内) | 中文分词要自己处理,排序粗糙 |
| 第三方搜索(Algolia 一类) | 数据推到别家服务器,调 API | 上万页、要拼写纠错与同义词 | 有配额、要同步数据、国内访问看运气 |本站这种规模(20 多个页面、数据都在 JSON 里)推荐第一种:构建期生成索引,部署就是多传一个目录,零后端、零数据库,也不依赖第三方活着。
一个前提先确认:索引器读的是最终 HTML,不是你脑子里的页面。如果正文是 JS 从 JSON 现渲染出来的(本站的向导步骤原本就是那样),爬虫和索引器都只能看到一个空壳——所以这一步之前必须先把内容静态化(本站用
scripts/seo-prerender.js把步骤正文生成进 HTML,索引才有东西可搜)。决定:页数不到 50 且内容本来就在 JSON 里 → 也可以直接跳到 step-4 的方案 C,省掉一条构建命令。
自查清单
- 已选定一种方案并说清理由
- 确认页面正文是静态 HTML(不是纯 JS 现渲染)
- 知道索引必须随每次发布重跑
-
Step 2 · 圈定正文:只让该被搜到的进索引
两条属性管住导航、页脚和广告位
索引器默认把页面上所有文字都收进去——包括导航、页脚、广告位、免责声明。结果就是搜「宝塔」出来 30 条全是页脚。
两条属性就能管住:
data-pagefind-body:标在正文容器上。一个页面只标一处;标了之后索引器只收这个容器里的内容(不标就整页全收)data-pagefind-ignore:标在要排除的块上(导航、页脚、广告位、noscript提示)
<main data-pagefind-body> <h1>标题</h1> <p>要被搜到的正文……</p> </main> <nav data-pagefind-ignore>导航</nav> <footer data-pagefind-ignore>页脚与免责声明</footer>顺手把元数据也标上,结果页才好看、才能筛选:
data-pagefind-meta="分类:建站"→ 结果里多一行说明data-pagefind-filter="分类:建站"→ 支持按分类过滤data-pagefind-index-attrs="alt,title"→ 图片的 alt 也进索引
标题与摘要:默认把
<h1>当标题、其后第一段当摘要。想自己定就用data-pagefind-meta="title:…"/data-pagefind-meta="summary:…"。本站的做法:场景落地页只把「步骤正文 + FAQ + 相关场景」三块放进
data-pagefind-body,顶部导航、底部推广位、noscript全部data-pagefind-ignore。验证:索引生成后搜一个只出现在页脚的词(比如「免责」),应该一条都搜不到。
自查清单
- 正文容器标了 data-pagefind-body,且全页只有一处
- 导航 / 页脚 / 广告位标了 data-pagefind-ignore
- 搜页脚独有的词搜不到结果
-
Step 3 · 生成索引:一条命令 + 中文分词
把索引钉进发布流程,别靠记得手动跑
一条命令生成索引(在站点根目录,也就是
index.html所在那层):npx pagefind --site .跑完多出一个
pagefind/目录,里面全是静态文件:pagefind.js/pagefind-ui.js:查询与官方 UIindex/*.pf_index+fragment/*.pf_fragment:索引分片,按需加载——用户搜什么才下什么,不会一次性拖垮首屏
中文:显式指定语言可避免多语种误判,用
--force-language zh-cn;若提示不支持该语言代码,去掉这个参数即可(索引器本身能处理 CJK)。参数以npx pagefind --help的实际输出为准。只索引该索引的:默认按 HTML 文件扫,需要限定范围时用
--glob(例如--glob "**/*.html");scripts/、data/这类目录不该进索引。每次改内容都要重跑索引——这是最容易忘的一步。把它钉进发布流程,本站的固定顺序是:
node scripts/seo-prerender.js # 1. 先把内容静态化(正文写进 HTML) npx pagefind --site . # 2. 再生成 / 更新索引 bash scripts/deploy-site.sh # 3. 最后连 pagefind/ 一起上传顺序不能反:先静态化后索引,反了索引到的是空壳页。
本地预览:
npx pagefind --site . --serve,或用任意静态服务器打开站点根目录搜一下试试。自查清单
- pagefind/ 目录已生成,里面有 pagefind.js
- 索引命令已写进发布流程(不是靠记得手动跑)
- 本地预览能搜出结果
-
Step 4 · 页面接入:官方 UI 或自己渲染
两种现成写法 + 一个零依赖自建方案
方案 A:官方 UI(十分钟能跑通)
<link rel="stylesheet" href="/pagefind/pagefind-ui.css"> <script src="/pagefind/pagefind-ui.js"></script> <div id="search"></div> <script> new PagefindUI({ element: '#search', showSubResults: true, translations: { placeholder: '搜索本站', zero_results: '没找到,换个词试试' } }); </script>方案 B:自己渲染(要定制样式走这个)
async function runSearch(term) { const pagefind = await import('/pagefind/pagefind.js'); const r = await pagefind.search(term); const data = await Promise.all(r.results.slice(0, 10).map((x) => x.data())); return data.map((d) => ({ title: d.meta.title, url: d.url, excerpt: d.excerpt })); }别让搜索拖慢首屏:
pagefind.js等用户第一次点搜索框时再import(),之前一个字节都不加载。方案 C:零依赖自建倒排(页数不到 50、内容本来就是 JSON 时最省事)
本站的场景数据都在
data/scenarios.json+data/steps.json里,不引入任何库也能搜:// 建索引:把每条记录的标题 + 正文切成词,记下出现在哪条 function buildIndex(docs) { const idx = new Map(); docs.forEach((d, i) => { const text = (d.title + ' ' + d.summary + ' ' + d.content).toLowerCase(); const words = text.match(/[a-z0-9]+|[\u4e00-\u9fa5]{1,2}/g) || []; words.forEach((w) => { if (!idx.has(w)) idx.set(w, new Set()); idx.get(w).add(i); }); }); return idx; } function query(idx, docs, term) { const words = term.toLowerCase().match(/[a-z0-9]+|[\u4e00-\u9fa5]{1,2}/g) || []; const score = new Map(); words.forEach((w) => (idx.get(w) || []).forEach((i) => score.set(i, (score.get(i) || 0) + 1))); return [...score.entries()] .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([i, n]) => ({ doc: docs[i], hit: n })); }中文怎么切:上面用「1–2 个字一组」的粗暴切法(bigram),对几十页的站够用——搜「宝塔」能命中「宝塔面板」,搜「备案」能命中「ICP备案」。想要更准就改用 Pagefind 或接分词库,别自己造分词器。
兜底:关掉 JS 时搜索不可用,但正文是静态的,照样能读——搜索只是增强,别让它变成唯一入口。
自查清单
- 搜索框能出结果
- pagefind.js 是懒加载,不拖慢首屏
- 关掉 JS 也能读到正文
-
Step 5 · 上线与观测:别让搜索页抢走收录
放行了才叫部署成功;零结果词就是选题清单
上传:
pagefind/是纯静态目录,和 HTML 一起传。传完先在浏览器直接访问域名/pagefind/pagefind.js与域名/pagefind/pagefind-ui.js——这两个能打开才算部署成功,打不开多半是服务器没放行该目录(宝塔站点配置里检查有没有目录访问限制)。别让搜索结果页去抢收录:搜索页(
/search.html、?q=xxx一类)是无限变化的低质量页面,提交给搜索引擎只会拉低索引比率:- 搜索页
<head>里加<meta name="robots" content="noindex,follow"> - 或在
robots.txt里写Disallow: /search sitemap.xml里不要放搜索页(本站 sitemap 由seo-prerender.js只收主页面 + 场景落地页)
观测三件事(口径写进
data/conversion.json,跟其他事件同一套埋点):search_query:有人搜就记词(不记个人信息)zero_result_rate:搜了但没结果的比例- 从搜索结果点进正文的比例
最有用的是零结果词:搜「宝塔」没结果,说明要么没这篇内容、要么标题里没这个词——这份清单直接就是选题清单。本站
ai-writing场景(选题 → 生成 → 过审 → 发布 → 观测)接的就是这个输入。验收:挑 5 个你确定站上有的词(场景名、工具名、步骤标题)逐个搜,5/5 都有结果才算做完。内容改了要重跑索引,把它写进发布清单,跟压缩 CSS/JS 一样当成必做步骤。
自查清单
- 浏览器能直接打开 /pagefind/pagefind.js
- 搜索页已 noindex,且不在 sitemap 里
- search_query 与零结果率已埋点
- 抽查 5 个已知词 5/5 命中
- 搜索页