Hugo实现简单站内搜索功能
对于静态站点,搜索功能应该是一个比较棘手的问题。对于内容比较庞大的网站,可能直接跳转到第三方搜索引擎比较适合。例如使用以下搜索指令:
关键词 site:www.beizigen.com
但这种方式容易造成用户跳出网站,体验也不如站内搜索好。另外,这种方式只能搜索到已被搜索引擎收录了的文章,新发表的文章则会被雪藏。
本文要介绍的是使用Fuse.js实现站内搜索,Fuse.js项目地址:
配置Hugo以支持搜索功能
修改Hugo配置文件,通常名称为hugo.yaml,参考Hugo配置文件说明。添加如下内容:
outputs:
home:
- HTML
- RSS
- JSON
在Hugo的content目录中创建search文件夹和_index.md文件:
/search/_index.md
_index.md的文件内容为:
---
title: 搜索
slug: search
---
在主题目录的如下路径创建index.json文件:
/layouts/_default/
index.json文件的内容为:
{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
{{- $.Scratch.Add "index" (dict "title" .Title "contents" .Plain "permalink" .Permalink) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
dict中的字段可以自定义,该字段决定要输出的内容,例如添加tags的输出:
"tags" .tags
添加日期的输出:
"date" .Date.Format .Site.Params.dateFormat
输出的json格式如下:
[
{
"contents": "纯文本的文章内容...",
"permalink": "文章URL",
"title": "文章标题"
},
...
]
模板文件配置
在要添加搜索框的模板文件中(通常是头部文件),添加搜索表单:
<form class="search" action="{{ relURL "/search" }}" method="GET">
<input type="search" name="q" placeholder="输入关键词搜索…">
<button type="submit" class="icon-search">搜索</button>
</form>
在主题目录的如下路径创建search.html模板文件:
/layouts/_default/
search.html模板的内容大致如下,具体根据你的网站布局调整:
{{ define "main" }}
<main id="main">
<div id="search-results">搜索中…</div>
<script src="{{ relURL "js/fuse.js" }}"></script>
<script src="{{ relURL "js/search.js" }}"></script>
</main>
{{ end }}
注意引用的两个js文件路径根据你的实际情况填写。
search.js的文件内容如下:
const options = {
keys: [
'title',
'contents'
]
};
const paginate = 10;
const summaryLength = 90;
const pattern = param('q');
if (pattern) {
fetch('/index.json')
.then(response => response.ok ? response.json() : undefined)
.then(search);
}
function search(json) {
let fuse = new Fuse(json, options);
let result = fuse.search(pattern);
let elem = document.querySelector('#search-results');
if (result.length > 0) {
let maxpage = Math.ceil(result.length / paginate);
let paged = param('p');
if (!paged || paged < 1) {
paged = 1;
}
if (paged > maxpage) {
paged = maxpage;
}
let start = (paged - 1) * paginate;
let html = '';
for (let i = start; i < start + 10; i++) {
if (!result[i]) continue;
let data = result[i].item;
html += '<article class="entry">';
html += '<h1><a href="' + data.permalink + '">' + data.title + '</a></h1>';
html += '<p>' + data.contents.substring(0, summaryLength) + '…</p>';
html += '</article>';
}
html += pagination(maxpage, paged, pattern);
elem.innerHTML = html;
} else {
elem.innerHTML = '<p>没有找到结果,可能你要搜索的内容已逃离地球</p>';
}
}
function param(name) {
let url = new URL(window.location);
return url.searchParams.get(name);
}
function pagination(maxpage, paged, pattern) {
if (maxpage <= 1) return '';
paged = parseInt(paged);
let baseurl = '/search/?q=' + pattern;
let pagination = '<ul class="pagination">';
if (paged > 1) {
pagination += '<li class="prev"><a href="' + baseurl + '&p=' + (paged - 1) + '">«</a></li>';
} else {
pagination += '<li class="prev disabled">«</li>';
}
let minpage = paged - 2;
if (minpage > maxpage - 4) minpage = maxpage - 4;
if (minpage < 1) minpage = 1;
for (let i = minpage; i < minpage + 5; i++) {
if (i > maxpage) break;
if (i == paged) {
pagination += ' <li class="disabled">' + i + '</li> ';
} else {
pagination += ' <li><a href="' + baseurl + '&p=' + i + '">' + i + '</a></li> ';
}
}
if (paged < maxpage) {
pagination += '<li class="next"><a href="' + baseurl + '&p=' + (paged + 1) + '">»</a></li>';
} else {
pagination += '<li class="next disabled">»</li>';
}
pagination += '</ul>';
return pagination;
}
实现原理解说
对Hugo的一系列配置是为了在网站根目录生成一个index.json,这个json文件包含了所有文章的纯文本内容。
Fuse.js是一个可以搜索json文件内容的库,options配置项定义要搜索的键,本文示例只搜索了文章标题和文章内容:
const options = {
keys: [
'title',
'contents'
]
}
编写了一个param函数用来获取URL中的搜索关键词,fetch网络请求获取index.json的文件内容。
Fuse.js的简单用法如下:
初始化一个Fuse新对象:
let fuse = new Fuse(json, options);
Fuse构造函数需要传两个实参:
- json:之前获取的index.json的内容;
- options:Fuse配置项,这里主要定义了要搜索的字段;
Fuse.js还有一些有意思的配置项,比如定义字段权重:
keys: [
{name: "title", weight: 2},
{name: "contents", weight: 1},
]
Fuse的search方法搜索内容并返回结果:
let result = fuse.search(pattern);
pattern为搜索关键词,搜索的结果保存在变量result中。
剩下的就是JS的一些Dom处理,将搜索结果(json数据)写入页面,这里我做了分页处理,可根据自己的实际情况来编写代码。
这种方式的缺陷是,随着网站内容的增多,index.json文件的体积会越来越大,这样就会导致网络请求延时较长。后期可以考虑index.json只输出文章标题和文章链接,但这样以来就会导致无法搜索文章内容,搜索结果质量会大幅下降。