为什么不用那些现成的
静态博客加搜索,常见三条路:
| 方案 | 要不要服务 | 中文支持 | 我的判断 |
|---|---|---|---|
| Algolia | 要注册、有免费额度 | 要自己配 | 依赖外部服务,哪天停服就废了 |
| 主题自带搜索 | 不用 | 基本按空格切词 | 中文等于不可用 |
| Pagefind | 不用 | 原生支持 | 构建时生成索引,纯前端查 |
Pagefind 最吸引我的点是:它在构建阶段把页面切块建索引,访客搜索时只下载命中的那几块,不是把整个索引一次性拉下来。对中文站尤其友好,因为它内置了分词处理,不像有些方案把中文当成一大坨无空格字符。
而且它是纯静态的,索引就是一堆文件放在 public/pagefind/ 里,跟着站点一起发布,没有任何外部依赖。
第一步:写配置文件
Pagefind 默认扫 _site,Hugo 输出的是 public,所以必须显式指定。我在博客根目录建了 pagefind.yml:
|
|
几个参数都是踩过才知道要加的,下面逐个说。
output_path 不能写错
一开始我图省事写成 pagefind,结果索引生成到了 public 的外面,站点访问 /pagefind/ 直接 404——文件根本没进发布目录。
必须写成 public/pagefind,让它落在构建产物内部。
force_language 才是中文能搜的关键
不加这一行,Pagefind 会按默认语言处理,中文被当成一个不含空格的长词,搜"人机"匹配不到"人机验证折腾记"。加上 zh 之后分词才正常。
root_selector 选大了会漏标题
这一行我改了两次:
- 一开始用主题的
.article-content,只索引正文 → 文章标题搜不到。因为<h1>在.article-content外面,短文章的正文又少,整个页面就显得没什么内容 - 改成
.main-article把标题一起包进来,问题解决
exclude_selectors 是防噪音
不排除的话,侧栏的标签云、目录、相关推荐、页脚全会被索引进去。搜"友链",出来一堆侧栏里的标签页,而不是那篇文章。把评论区也排掉,否则读者留言里的文字会污染搜索结果。
第二步:构建流程变成两步
Pagefind 是读已构建的 HTML 建索引的,所以顺序不能反:
|
|
npx -y 省掉全局安装,每次用最新版。索引生成完,public/pagefind/ 里就是一堆 .pf_fragment 之类的文件,随站点一起推上去。
第三步:初始化搜索界面
Pagefind 提供了现成的 UI 组件,引两个文件就能用:
|
|
translations 能把界面文案全换成中文,不然输入框里写着 “Search”,跟整站风格割裂。
第四步:四个坑
到这里功能是通的,但在我这个开了无刷新跳转(PJAX)的站上,问题一个接一个冒出来。
坑一:从首页跳进搜索页,搜索框是白框
PJAX 只替换 .main-container 里的内容,<head> 不换。所以 /pagefind/pagefind-ui.css 这个 <link> 根本没被加载,搜索框出来是裸的 HTML,白底黑字毫无样式。
修法是动态注入 CSS:每次挂载前检查 <link> 在不在,不在就用 JS 创建一个插进 head。
|
|
所以这一段 JS 我放在了 footer/custom.html,也就是 .main-container 外面——PJAX 不替换这块,脚本能一直活着。
坑二:偶发"索引没配置"
现象很怪:刷新几次,有时候正常有时候报错,说找不到索引。
原因是竞态。pagefind-ui.js 是异步加载的,脚本执行到 new PagefindUI(...) 时,那个文件可能还没下载完,window.PagefindUI 还是 undefined。既然不报错也不重试,就直接挂了。
修法是轮询等待:
|
|
100 次 × 100ms,最多等 10 秒。等到了就挂载,等不到就在页面上显示"搜索资源加载失败,请刷新重试",总比一片空白强。
坑三:脚本重复插入
ensureScript 里加了个 data-pf-ui 属性做标记:
|
|
不加这个标记,PJAX 每次换页都插一个新 script,浏览器会重复执行,页面上叠出好几个搜索框。
坑四:重复挂载
这个和上面配套——script 不重复了,但挂载逻辑还是会跑多次。用 dataset 记个状态:
|
|
挂载前先清空容器,免得旧搜索框残留。
顺手加的一个功能:URL 带关键词
搜索结果页如果能直接分享链接,体验会好很多,比如 /search/?keyword=Hugo。
从 URL 读参数,塞进输入框,再手动派发一个 input 事件触发搜索:
|
|
这里有个细节:不能只设 input.value。直接赋值不会触发框架的事件监听,搜索框里显示着关键词但结果区是空的。必须 dispatchEvent 一个 input 事件,Pagefind 才会真的去查。
最后:别忘了 PJAX
所有挂载逻辑都挂在 pjax:complete 上,换页后重新走一遍:
|
|
setTimeout(..., 0) 是为了等新 DOM 渲染完再操作,不然拿到的还是旧节点。
小结
按耗时排一下,真正麻烦的不是 Pagefind 本身,而是无刷新站点和第三方组件的配合:
| 问题 | 根因 | 解法 |
|---|---|---|
| 中文搜不到 | 没做分词 | force_language: zh |
| 标题搜不到 | 索引范围漏了 h1 | root_selector: .main-article |
| 白框无样式 | PJAX 不换 head | 动态注入 CSS |
| 偶发索引报错 | 异步竞态 | 轮询等 PagefindUI |
| 多个搜索框 | 脚本重复插入 | data-pf-ui 标记 + 挂载状态位 |
Pagefind 本身配置简单,中文支持也是现成的。真正要花心思的,是让它在不是整页刷新的环境里正常工作。
参考:pagefind.app 官方文档