前言 ¶
我早就⧉想要给自己这个博客站添加站内搜索功能了,前前后后也考察过非常多的工具,官方列出的选择⧉就非常之多,但没有令我满意的:如果想要开箱即用,就要用 npm 安装 Nodejs 的一些工具;稍微简单点办法的就是利用 hugo 能够生成 json 的能力(教程一⧉、教程二⧉),但需要引入第三方搜索库,自己还要写不少的代码来实现. 所以我就一直拖着没有弄.
直到上个月我在读阮一峰的科技爱好者周刊(第 226 期)⧉的时候才偶然看到这么一个工具:Pagefind⧉. 我花了半个小时仔细考察了它,几乎是立刻就确定了它正是我恰好想要、且一直在等待的那个工具. 它吸引到我的有 5 点:
- 它上了 HugoConf 2022⧉,得到了 Hugo 社区的认可. 阅读它的文档时也能感受得到,这个工具一开始就是为了 Hugo 而设计的;
- 它的开发语言是 Rust,最后所提供出来的产品就是一个简单的二进制可执行文件,不需要安装环境或配置依赖,开箱即用,非常方便;
- 它的多语言⧉支持做得不错:有专门对中文的分词索引支持. 虽然暂不支持查询字段分词,但正努力开发中;
- 它自带了一个简单的 HTTP server,本地调试起来很是方便. 如果是从头建站,它提供了十分钟就能用上的还很好看的 Pagefind UI;即便要自己写,它也提供了简单易用的接口;
- 它充分考虑了大型网站的搜索,将索引分成了不同的小 chunks,节省读者的网络流量,比起用 hugo 直接生成 json 要简洁、优雅得多.
交互设计 ¶
第一个问题是如何设计交互:搜索框应该放在哪里?搜索的流程与步骤该是什么样?宽屏幕怎么做,小屏幕又怎么做?
我仔细考察了很多包含搜索功能的网站,现在大家都已经是竖版的布局了,在页面的最上面有一个横着的顶栏,搜索框就放在这里. 当读者键入一些内容之后,交互上的做法大致可以分成三类:
- 第一种是会出现一个专门的区域存放搜索结果,悬浮于整个页面的最顶层(举例:Hugo 的官方文档⧉、维基百科⧉);
- 第二种是把搜索结果直接渲染到页面上,把当前网页上的其他内容往下挤(举例:Pagefind 的示例站点⧉);
- 第三种是在键入内容时不会有任何交互,必须要点击一下搜索按钮或按下回车键之后才开始搜索,此时一般会跳转到一个单独页面用来陈列搜索结果(举例:GitHub⧉、量子位⧉、思否⧉、简书⧉).
在 UI 设计上,这个搜索框可能只显示为一个搜索按钮,单击后才展开成为一个搜索框(举例:LoveIt 主题⧉、MemE 主题⧉);或者单击后弹出一个搜索区域,里面囊括更多高级搜索的功能(举例:Quanta Magazine⧉、少数派⧉). 它们的后续交互还是要归结于上面的那三种,相当于是多了一个操作步骤.
我认为:直接显示搜索框,且键入内容之后马上就能看到搜索结果的交互设计才是最方便的,体验也是最好的,其中又属第一种交互模式最为优雅、合理,不破坏整体布局. 但是如何在我这个左右两栏布局的网页里实现第一种交互模式就成了新的问题:按道理,搜索功能是每个页面都有的公共的功能,应该放在左边的公共的导航栏里;然而左边本来就空间狭小、信息密集,如果再将搜索结果悬浮于左边的导航文字之上,会显得十分逼仄,感觉也破坏了左栏的导航功能.
经过仔细考虑,我决定借鉴 Quanta Magazine 的设计模式:写一个单独的搜索区域,但把它先给隐藏起来,只在左边的导航区留下一个小小的放大镜符号作为搜索入口. 当读者点击搜索按钮时,使用纯前端的技术,将整个左边栏完全替换为那个单独的搜索区域. 正因为不是悬浮于导航栏之上而是覆盖掉整个导航栏,视觉上会干净、清爽得多,同时后续搜索流程上的交互模式也可以完全按照第一种办法来实现,搜索体验也会是相当不错的.
对于小屏幕来说,先点开左栏才能看到搜索按钮,再点击搜索按钮才能看到搜索区域,这无疑是过于麻烦的. 因此可以直接把搜索按钮放在顶栏里,点击后把下面整个页面都用搜索区域替换掉.
前端实现 ¶
既然是所有页面都共有的功能,那自然是要写到 base.html
里的. 但由于宽屏幕和小屏幕的搜索区域位置不同,里面的功能却完全一致,所以如果直接写进对应的位置就要写两遍,无法复用代码;而且最开始这个搜索区域应该是隐藏起来的,读者点击搜索按钮后才会出现,这就导致,如果把代码直接写进对应的位置,页面渲染时会先渲染出搜索区域,然后马上才应用 CSS 样式表将其隐藏,观感就是搜索区域整个一闪而过了一下,非常不美观. 我的解决方案就是,将搜索区域写成一个 HTML 的 template⧉,等整个页面加载完成后(样式表已经应用后)再用 JavaScript 把它装载进相应的位置. 代码结构与原理如下:
|
|
|
|
这里利用这个 prefix
变量,可以精确地将左右两个搜索区域分开,为了放便起见可以直接在 JavaScript 中更改对应的 id 字段. 调整样式则是根据 search_show
这个 class 完成隐藏与显示,具体的代码不再赘写. 这样一来,搜索入口、整个搜索区域、搜索框、关闭搜索的按钮以及对应的打开搜索区域、关闭搜索区域的功能,在这里就算是完成了.
接入 Pagefind ¶
Pagefind 只做两件事:索引和搜索. 索引是 Hugo 每次生成完 public/
静态文件之后,Pagefind 将其作为输入,按照相应的规则,把相应的内容编进索引,最后输出成一个单独的 _pagefind/
文件夹;搜索则是关于读者在网站上具体搜索时的展示. 打个非常不恰当的比方:索引是后端,搜索是前端.
索引 ¶
按照上面的解释,在索引这一块儿,我们要做的就是根据自己的需要,编写相应的索引规则,确定需要索引的内容以及应该排除在外的内容.
首先是利用 data-pagefind-body
框定索引范围(参考⧉). 在我这个博客里,我希望把索引范围框定在文章页的右边区域,也就是说,主页和列表页不参与索引,文章页的左侧导航区也不参与索引. 因此我这样写:
|
|
接着把一些边边角角的不想被索引到的东西用 data-pagefind-ignore
指定一下. 在我的博客里,我考虑到的有:发表时间、字数统计、同栏目下的友情链接、数学笔记的 PDF 区域、评论区(生成的 public/
目录中这个位置本来也是空的,但还是写进去了)、目录. 书影音记录页面,我只索引了“书名”、“电影名”、“我的评价”三个板块的内容,其余内容全部忽略掉了. 接着考虑几个 render hooks:标题忽略章节符号“¶”,链接忽略外链符号“⧉”,图片不需要额外添加索引,因为我有 <figcaption>
标签,已经可以被索引到了.
这里还有加密区域也应该忽略掉索引,在 layouts/shortcodes/hugo-encrypt.html
里,有一个专门的 <hugo-encrypt>
标签,在它后面也添加上 data-pagefind-ignore
就行了. 不过我的加密功能是写在博客源码里而非主题源码里的,这里其实已经不完全是主题的更改了.
Pagefind 还提供了过滤器⧉功能,十分强大. 针对我这个博客,我则是用它把所有的索引结果打上了所属的 section 的标签,这样一来在展示搜索结果时就可以对文章所属的两级 section 进行展示. 我这个主题在左边栏判断 section 的选中状态(class="active"
)时就已经写好了相关的逻辑,这里为了代码复用,把原来的逻辑提炼出来,存成两个新的变量($if_1_on
和 $if_2_on
)即可.
|
|
注意到这里 data-pagefind-filter
标签并不需要一定在 <body>
中或 data-pagefind-body
标签下,所以我能够在这里把它写在左边的导航区,省了不少事儿. 感谢 Pagefind 开发者的周到考虑.
Pagefind 还有 metadata⧉ 也需要额外配置一下,主要就是文章标题和日期. Pagefind 默认会取第一个 <h1>
标签作为 metadata 标题,我这个主题中文章标题是单独一个容器,<h1>
标签并不归文章标题所使用(startLevel
为 1,Hugo 文档⧉),所以这里需要重写;Pagefind 的默认 metadata 中也没有日期字段,这里也给它添加上:
|
|
标题那里在双大括号里使用了短横线⧉,以免取到的题目左右有空格或回车.
Pagefind 在索引这件事上还有多语言⧉以及搜索顺序⧉可供配置,我这个博客不需要配置这些,它提供的默认选项已经则够好.
搜索 ¶
搜索是读者那边的前端交互模块了,Pagefind 自带了一个开箱即用的 Pagefind UI,好看且功能强大,刚开始建站写主题的话可以直接拿过来用. 不过我的前端设计以及交互模式完全是我自己设计的,难以把它这套东西强行塞进去,所以我完全放弃了它提供的这一套 Pagefind UI,转而全都自己写. 好在 Pagefind 也提供了强大的搜索接口⧉,写起来反而是更加容易的.
其实这里的[搜索功能的实现]本质上就是定义了下面这个 PagefindSearch(text, canvas)
函数,它的入参 text
是读者在搜索框里所打的文字,canvas
是展示搜索结果的区域,这个函数的功能就是去调用 Pagefind 接口搜索 text
字段,并将结果渲染到 canvas
里面. 这段代码只是一个示例,没有详细展示渲染的逻辑,不过其最佳实践仍然是使用 template 技术.
|
|
前面前端实现那个小节写过一个 showSearch
函数,这里稍微扩充一下,以监听用户的输入,并利用函数防抖(debounce)⧉延迟 600 毫秒把读者的输入传进我们刚刚写的那个 PagefindSearch(text)
函数中:
|
|
这样一来,整个前端的搜索工作就也完成了. 这里写得比较简略,实际上还要单独设计搜索结果的展示区域的样式等等.
调试 ¶
最后再提一下调试的流程. 因为 Pagefind 是在 Hugo 生成了所有静态文件之后才去建立索引的,所以调试起来稍微有一点麻烦. 每当做了一点改动之后,我会先关掉 Pagefind 自带的 HTTP 服务,删除 public
文件夹下的所有内容:
rm -rf ./public/*
然后重新生成静态文件:
hugo
最后重新利用 Pagefind 建立索引并开启新的 HTTP 服务:
./pagefind_extended --serve
刷新一下网页(127.0.0.1:1414
),就可以对刚刚修改的内容做个测试了.
最后的搜索效果如下:
其他问题 ¶
这里有两个问题也该说明一下. 一是 Pagefind 也是有自己的配置文件⧉的. 像我在终端里可以直接运行 ./pagefind_extended
而不用带 --source public
这个 flag 就是因为我把 source
定义在配置文件里面了.
二是,引入 Pagefind 所需要的关键模块时使用的语句是 import("/_pagefind/pagefind.js");
,而在 Hugo 生成静态文件的过程中,这个模块是不存在的,也就是说,这个 /_pagefind/pagefind.js
文件是 Pagefind 建立索引时才自动生成的. 这就导致了 Hugo 主题里是没法对 main.js
做 minify⧉ 的. 我认为解决办法有二:要么就把 Pagefind 相关的 JavaScript 代码(包含 import 的最小代码单位)单独拿出去独立为一个新的脚本;要么就应该由 Pagefind 项目组考虑到这个问题,release 出一个单独的 pagefind.js
文件(我自己从生成的文件夹里拷贝也应该可行),放在主题的 assets/
文件夹里.
友情链接(该栏目下的其他文章)
- 第 1 期:Hugo 静态博客建站记 - 自写主题
- 第 2 期:博客记录书影音——Hugo 的 Data Templates - 自写主题
- 第 3 期:如何让静态网站的暗色主题换页不闪烁 - 自写主题
- 第 4 期:Hugo 如何让图片在暗色主题下反色 - 自写主题
- 第 5 期:利用 Pagefind 给静态博客添加搜索功能 - 自写主题(就是这篇!)