Hugo实现简单站内搜索功能

对于静态站点,搜索功能应该是一个比较棘手的问题。对于内容比较庞大的网站,可能直接跳转到第三方搜索引擎比较适合。例如使用以下搜索指令:

关键词 site:www.beizigen.com

但这种方式容易造成用户跳出网站,体验也不如站内搜索好。另外,这种方式只能搜索到已被搜索引擎收录了的文章,新发表的文章则会被雪藏。

本文要介绍的是使用Fuse.js实现站内搜索,Fuse.js项目地址:

https://fusejs.io/

配置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="输入关键词搜索&hellip;">
    <button type="submit" class="icon-search">搜索</button>
</form>

在主题目录的如下路径创建search.html模板文件:

/layouts/_default/

search.html模板的内容大致如下,具体根据你的网站布局调整:

{{ define "main" }}
<main id="main">
    <div id="search-results">搜索中&hellip;</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) + '&hellip;</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) + '">&laquo;</a></li>';
	} else {
		pagination += '<li class="prev disabled">&laquo;</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) + '">&raquo;</a></li>';
	} else {
		pagination += '<li class="next disabled">&raquo;</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只输出文章标题和文章链接,但这样以来就会导致无法搜索文章内容,搜索结果质量会大幅下降。

阿里云