最近发现博客文章数量慢慢上来了,有时候想找自己以前写的某个知识点,翻半天都翻不到,确实有点不方便。

其实早就想加个搜索功能了,但看了一圈,像 Algolia 这种第三方服务虽然强大,但配置起来感觉有点繁琐,而且对于咱们这种访问量不大的静态博客来说,是不是有点“杀鸡用牛刀”了?

想了想,还是决定用最简单的方式来实现:直接生成一个 JSON 索引文件,用 JS 在前端搜一下。虽然比较基础,但胜在轻量,而且完全可控。

折腾了几个小时,终于搞定了,顺便把交互做得稍微优化了一下,支持了 PJAX 和暗黑模式。这里简单记录一下实现过程,希望能给同样在折腾 Hugo 的朋友一点参考。

效果展示

为了不破坏页面的简洁感,我把搜索入口藏在了两个地方:

  1. 右下角悬浮工具栏:点那个放大镜图标,会弹出一个搜索框。
  2. 首页“随笔”标题:这个算是个小彩蛋吧。在首页找到“随笔”这两个字,鼠标放上去会有“点击搜索”的提示,点一下就能直接在原地搜索,不用跳转。

实现思路

原理其实非常简单,主要就三步:

  1. 让 Hugo 构建时多生成一个 index.json 文件,里面包含所有文章的标题、链接和摘要。
  2. 前端 JS 通过 fetch 请求这个 JSON 文件。
  3. 根据用户输入的关键词,在 JSON 数据里进行匹配,然后把结果渲染出来。

代码实现

1. 配置 Hugo 输出 JSON

首先在 hugo.toml (或者 config.toml) 里配置一下输出格式,告诉 Hugo 首页除了 HTML 还要输出 JSON。

[outputs]
  home = ["HTML", "RSS", "JSON"]

2. 创建 JSON 模板

在主题的 layouts/index.json 创建一个模板文件,定义 JSON 的数据结构。这里我只取了标题、日期、链接和摘要,尽量让文件小一点。

[
    {{- range $index, $e := where .Site.RegularPages "Type" "post" -}}
    {{- if $index -}}, {{- end -}}
    {
        "title": {{ .Title | jsonify }},
        "date": {{ .Date.Format "2006-01-02" | jsonify }},
        "permalink": {{ .Permalink | jsonify }},
        "summary": {{ .Summary | plainify | jsonify }}
    }
    {{- end -}}
]

这样每次 hugo 构建的时候,网站根目录就会生成一个 index.json

3. 前端 JS 逻辑

逻辑主要在 main.js 里。为了提升体验,我做了一些小优化:

  • 懒加载:不会一打开网页就下载 JSON,而是等你点击搜索按钮时才去加载,帮大家省点流量。
  • 防抖处理:输入时不会每敲一个字都去搜索,而是稍微停顿一下再触发,避免频繁计算。
  • PJAX 适配:因为博客用了 PJAX 做无刷新跳转,一开始遇到个坑,点击搜索结果页面会白屏或者刷新。后来发现需要在渲染结果后,手动调用一下 window.pjax.refresh(),让 PJAX 重新接管这些新生成的链接。
// 简单的搜索逻辑
const results = window.searchIndex.filter(item => {
    // 简单的关键词匹配
    return item.title.includes(query) || item.summary.includes(query);
});
displayResults(results);

4. 那个“随笔”变搜索框的交互

这个交互其实主要是 CSS 在控制。

默认是一个普通的 <h2> 标题,点击后通过 JS 隐藏标题,显示输入框。为了过渡自然一点,加了一些简单的 CSS 动画。

/* 默认隐藏提示 */
.search-hint {
    opacity: 0;
    transition: all 0.3s ease;
}

/* 鼠标放上去显示 */
.section-header.normal-mode:hover .search-hint {
    opacity: 1;
    transform: translateX(0);
}

为了照顾移动端用户,我在 CSS 里加了媒体查询,手机上直接显示“点击搜索”的提示,不然手机上没有 hover 状态,可能会不知道这里可以点。

遇到的坑

折腾过程中最大的坑还是 PJAX

一开始写好的时候,搜索功能本身没问题,但是点搜索结果跳转的时候,要么页面刷新,要么音乐播放器断了。查了半天文档,才发现是因为动态插入的 HTML(搜索结果列表),PJAX 默认是监听不到的。

解决办法就是在插入 HTML 后,手动通知一下 PJAX:

if (window.pjax) window.pjax.refresh(searchResults);

另外,为了体验好一点,我还加了个逻辑:点击搜索结果后,自动关闭搜索框,不然跳转完搜索框还挡在那里,确实有点傻。

总结

虽然只是个小功能,但前前后后也调了好久,特别是细节上的打磨。不过看到最后丝滑的搜索体验,感觉还是挺值得的。

如果你也在用 Hugo,不想接复杂的第三方搜索,不妨试试这个方案,轻量、简单,完全够用了。

代码就不全贴了,核心逻辑都在上面,有兴趣的朋友可以自己折腾一下。如果有更好的实现方式,也欢迎留言交流哈!