静态博客搜索的难点
静态博客的所有页面都是预生成的 HTML 文件,没有后端服务器和数据库,因此无法像动态网站那样执行 SQL 查询来实现搜索。常见的静态站搜索方案有三种:
- 第三方服务(Algolia、DocSearch)
- 客户端全文搜索库(Fuse.js、Lunr.js)
- 自建 JSON 索引 + 前端搜索
本文介绍的是第三种方案:在 Hugo 构建阶段生成包含全站文章内容的 JSON 索引文件,前端 JavaScript 加载该索引并在浏览器端完成搜索。
Hugo 端:生成 JSON 索引
首先在 hugo.toml 中配置首页输出 JSON:
1[outputs]
2 home = ["HTML", "RSS", "JSON"]
然后在主题的 layouts/_default/index.json.json 中创建模板:
1{{- $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}}
2[
3 {{- range $index, $page := $pages -}}
4 {{- if $index }},{{ end }}
5 {
6 "title": {{ $page.Title | jsonify }},
7 "url": {{ $page.Permalink | jsonify }},
8 "date": {{ $page.Date.Format "2006-01-02" | jsonify }},
9 "content": {{ $page.Plain | truncate 500 | jsonify }},
10 "categories": {{ $page.Params.categories | jsonify }},
11 "tags": {{ $page.Params.tags | jsonify }}
12 }
13 {{- end }}
14]
这段模板会在站点根目录生成 /index.json,包含每篇文章的标题、URL、日期、摘要内容和分类标签。
前端:搜索逻辑实现
前端搜索的核心流程:
- 加载索引:页面初始化时 fetch 加载
/index.json - 监听输入:监听搜索框的 input 事件
- 执行搜索:在内存中对标题和内容做关键词匹配
- 渲染结果:动态生成搜索结果列表的 DOM 节点
搜索算法
最简单的实现是对每条记录的 title 和 content 字段做字符串包含匹配:
1function search(keyword, records) {
2 const lower = keyword.toLowerCase();
3 return records
4 .filter(record =>
5 record.title.toLowerCase().includes(lower) ||
6 record.content.toLowerCase().includes(lower)
7 )
8 .slice(0, maxResults); // 限制最大结果数
9}
如果需要更好的搜索体验,可以引入 Fuse.js 库实现模糊匹配:
1const fuse = new Fuse(records, {
2 keys: [
3 { name: 'title', weight: 2 }, // 标题权重更高
4 { name: 'content', weight: 1 },
5 { name: 'tags', weight: 1.5 }
6 ],
7 threshold: 0.4, // 模糊度阈值
8 distance: 100
9});
10
11const results = fuse.search(keyword);
搜索框 UI
搜索框通常放在导航栏或侧边栏。点击或按 Ctrl+K 快捷键弹出搜索面板:
1<div class="search-overlay" id="search-overlay">
2 <div class="search-panel">
3 <input type="text" id="search-input"
4 placeholder="搜索文章..."
5 autocomplete="off">
6 <div class="search-results" id="search-results"></div>
7 </div>
8</div>
键盘快捷键
增强用户体验的键盘操作:
| 快捷键 | 操作 |
|---|---|
Ctrl + K / Cmd + K | 打开搜索 |
↑ ↓ | 在结果中上下移动 |
Enter | 打开选中文章 |
Escape | 关闭搜索 |
性能优化
索引文件大小控制
如果文章数量较多(>100 篇),JSON 文件可能很大。优化策略:
- 内容截断:每篇文章只保留前 200-300 个字符
- 移除不必要的字段
- 开启 Gzip:Nginx 或 CDN 层开启 gzip 压缩,JSON 文件压缩率通常 >80%
搜索结果数量
通过 maxResults 限制返回数量(默认 30),避免渲染过多 DOM 节点。同时使用防抖(debounce)减少搜索频率:
1const debouncedSearch = debounce(function(keyword) {
2 renderResults(search(keyword, records));
3}, 200);
4
5input.addEventListener('input', function() {
6 debouncedSearch(this.value);
7});
总结
基于 JSON 索引的前端搜索方案具备以下优势:
- 零成本:无需第三方服务订阅费用
- 离线可用:索引加载后可离线搜索
- 完全可控:搜索逻辑和 UI 都可以自定义
- 构建时生成:索引随 Hugo 构建自动更新,是最新的内容
本站搜索功能即采用此方案,支持全站文章标题和正文的即时搜索,最大搜索结果数可配置,同时支持键盘快捷键操作。
留言评论
期待你的想法评论加载中