为Hugo博客添加搜索功能
起因
一直以来我对博客的功能要求都不高,比如说 wordpress 上一些强大的主题,虽然有些功能的确是很吸引人,但我觉得很多都是可有可无的,博客发展到现在,基本都已经成了个人自娱自乐的自留地了,所以大家都各有各的选择,没有好与不好之分,只要适合就好。
昨天有个博友圈的朋友和我说,为什么你的博客连个搜索功能都没有的啊?太 Low 了。
其实嘛,我现在用的主题是带搜索功能的,不过是使用 google 的 ces,所以国内都使用不了,我就把功能给关了。刚开始我也试着去修改,曾经看中了 Algolia,但当时可能是太长时间没折腾了,CSS 上一些基础知识都忘得七七八八了,所以一直没能把 Algolia 很好地融入到现在这个主题上。
也许是因为最近折腾多了,慢慢的也找回了点记忆,外加朋友也正好说起了这个事情,就尝试着去为博客添加一个搜索功能吧,毕竟搜索也属于博客的一个基本功能,就如评论一样,没有的话总感觉少了丝灵魂。
说明
去Hugo 官网文档找了下,这个怎么说呢,可选择的还真不是很多,有些还是三年没有更新过了的。最后我选择了hugofastsearch,官网文档是这样定义的:
A usability and speed update to “GitHub Gist for Fuse.js integration” — global, keyboard-optimized search.
没错,这个搜索功能其实就是官方文档中另外一个方案“GitHub Gist for Fuse.js integration”的升级改良版。至于有什么优缺点,老实说我也是刚用,具体好与坏现在也不好说,我当时选择这个方案是因为不用其他的依赖,也不用再输入任何的编译命令,更不用其他的工具。所以正适合追求简洁的我。
安装
添加 index.json
在 themes/主题文件夹/layouts/_default 添加一个 index.json,内容为
{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
{{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "categories" .Params.categories "contents" .Plain "permalink" .Permalink "date" .Date "section" .Section) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
修改配置文件 config.toml
[outputs]
home = ["HTML", "RSS", "JSON"]
添加 js 文件
在 Hugo 默认的静态文件目录/static/js/添加 fastsearch.js 和 fuse.js。fuse.js 这里建议使用官方提供 fuse.min.js,下载地址:https://github.com/krisk/fuse
fastsearch.js 内容如下
var fuse; // holds our search engine
var fuseIndex;
var searchVisible = false;
var firstRun = true; // allow us to delay loading json data unless search activated
var list = document.getElementById('searchResults'); // targets the <ul>
var first = list.firstChild; // first child of search list
var last = list.lastChild; // last child of search list
var maininput = document.getElementById('searchInput'); // input box for search
var resultsAvailable = false; // Did we get any search results?
// ==========================================
// The main keyboard event listener running the show
//
document.addEventListener('keydown', function (event) {
// CMD-/ to show / hide Search
if (event.altKey && event.which === 191) {
// Load json search index if first time invoking search
// Means we don't load json unless searches are going to happen; keep user payload small unless needed
doSearch(event);
}
// Allow ESC (27) to close search box
if (event.keyCode == 27) {
if (searchVisible) {
document.getElementById('fastSearch').style.visibility = 'hidden';
document.activeElement.blur();
searchVisible = false;
}
}
// DOWN (40) arrow
if (event.keyCode == 40) {
if (searchVisible && resultsAvailable) {
console.log('down');
event.preventDefault(); // stop window from scrolling
if (document.activeElement == maininput) {
first.focus();
} // if the currently focused element is the main input --> focus the first <li>
else if (document.activeElement == last) {
last.focus();
} // if we're at the bottom, stay there
else {
document.activeElement.parentElement.nextSibling.firstElementChild.focus();
} // otherwise select the next search result
}
}
// UP (38) arrow
if (event.keyCode == 38) {
if (searchVisible && resultsAvailable) {
event.preventDefault(); // stop window from scrolling
if (document.activeElement == maininput) {
maininput.focus();
} // If we're in the input box, do nothing
else if (document.activeElement == first) {
maininput.focus();
} // If we're at the first item, go to input box
else {
document.activeElement.parentElement.previousSibling.firstElementChild.focus();
} // Otherwise, select the search result above the current active one
}
}
});
// ==========================================
// execute search as each character is typed
//
document.getElementById('searchInput').onkeyup = function (e) {
executeSearch(this.value);
};
document.querySelector('body').onclick = function (e) {
if (e.target.tagName === 'BODY' || e.target.tagName === 'DIV') {
hideSearch();
}
};
document.querySelector('#search-btn').onclick = function (e) {
doSearch(e);
};
function doSearch(e) {
e.stopPropagation();
if (firstRun) {
loadSearch(); // loads our json data and builds fuse.js search index
firstRun = false; // let's never do this again
}
// Toggle visibility of search box
if (!searchVisible) {
showSearch(); // search visible
} else {
hideSearch();
}
}
function hideSearch() {
document.getElementById('fastSearch').style.visibility = 'hidden'; // hide search box
document.activeElement.blur(); // remove focus from search box
searchVisible = false;
}
function showSearch() {
document.getElementById('fastSearch').style.visibility = 'visible'; // show search box
document.getElementById('searchInput').focus(); // put focus in input box so you can just start typing
searchVisible = true;
}
// ==========================================
// fetch some json without jquery
//
function fetchJSONFile(path, callback) {
var httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = function () {
if (httpRequest.readyState === 4) {
if (httpRequest.status === 200) {
var data = JSON.parse(httpRequest.responseText);
if (callback) callback(data);
}
}
};
httpRequest.open('GET', path);
httpRequest.send();
}
// ==========================================
// load our search index, only executed once
// on first call of search box (CMD-/)
//
function loadSearch() {
console.log('loadSearch()');
fetchJSONFile('/index.json', function (data) {
var options = {
// fuse.js options; check fuse.js website for details
shouldSort: true,
location: 0,
distance: 100,
threshold: 0.4,
minMatchCharLength: 2,
keys: ['permalink', 'title', 'tags', 'contents']
};
// Create the Fuse index
fuseIndex = Fuse.createIndex(options.keys, data);
fuse = new Fuse(data, options, fuseIndex); // build the index from the json file
});
}
// ==========================================
// using the index we loaded on CMD-/, run
// a search query (for "term") every time a letter is typed
// in the search box
//
function executeSearch(term) {
let results = fuse.search(term); // the actual query being run using fuse.js
let searchitems = ''; // our results bucket
if (results.length === 0) {
// no results based on what was typed into the input box
resultsAvailable = false;
searchitems = '';
} else {
// build our html
// console.log(results)
permalinks = [];
numLimit = 5;
for (let item in results) {
// only show first 5 results
if (item > numLimit) {
break;
}
if (permalinks.includes(results[item].item.permalink)) {
continue;
}
// console.log('item: %d, title: %s', item, results[item].item.title)
searchitems =
searchitems +
'<li><a href="' +
results[item].item.permalink +
'" tabindex="0">' +
'<span class="title">' +
results[item].item.title +
'</span></a></li>';
permalinks.push(results[item].item.permalink);
}
resultsAvailable = true;
}
document.getElementById('searchResults').innerHTML = searchitems;
if (results.length > 0) {
first = list.firstChild.firstElementChild; // first result container — used for checking against keyboard up/down location
last = list.lastChild.firstElementChild; // last result container — used for checking against keyboard up/down location
}
}
添加 HTML 代码到主题里
这里因为每个主题可能存在差异,所以请根据自己实际的情况做出相应的更改。我选择将代码添加到页头的菜单栏后面,在/layouts/partials/header.html 添加
<li class="menu-item">
<a id="search-btn" style="display: inline-block;" href="javascript:void(0);">
<i class="iconfont"> {{ partial "svg/search.svg" }} </i>
</a>
<div id="fastSearch">
<input id="searchInput" tabindex="0" />
<ul id="searchResults"></ul>
</div>
</li>
li 标签是继承主题,i 标签是因为调用了图标。
在主题模板上引用 js
我使用的主题有一个专门引用 js 的模板,所以我选择在此添加引用。选择/layouts/partials/scripts.html 添加
<!-- Fastsearch -->
<script src="/js/fuse.min.js"></script>
<script src="/js/fastsearch.js"></script>
添加 CSS 样式
添加样式我们尽量选择对应的模板来添加,比如说我是在 header 里修改的,那么我就直接选择在/assets/sass/_partial/_header.scss 添加 CSS 样式了。如果你使用的主题没有模板 CSS 的话,直接在主题的主 CSS 上添加。
#fastSearch {
visibility: hidden;
position: absolute;
right: 0px;
top: 30px;
display: inline-block;
width: 320px;
margin: 0 10px 0 0;
padding: 0;
}
#fastSearch input {
padding: 4px;
width: 100%;
height: 31px;
font-size: 1em;
color: #465373;
font-weight: bold;
background-color: #95b0f4;
border-radius: 3px 3px 0px 0px;
border: none;
outline: none;
text-align: left;
display: inline-block;
}
#fastSearch ul {
list-style: none;
margin: 0px;
padding: 0px;
}
#searchResults li {
list-style: none;
margin-left: 0em;
background-color: #e1e7f7;
border-bottom: 1px dotted #465373;
}
#searchResults li .title {
font-size: 0.9em;
margin: 0;
display: inline-block;
}
#searchResults {
visibility: inherit;
display: inline-block;
width: 328px;
margin: 0;
max-height: calc(100vh - 120px);
overflow: hidden;
}
#searchResults a {
text-decoration: none !important;
padding: 10px;
display: inline-block;
width: 100%;
}
#searchResults a:hover,
#searchResults a:focus {
outline: 0;
background-color: #95b0f4;
color: #fff;
}
#search-btn {
position: sticky;
font-size: 20px;
}
这里需要根据自己主题来进行稍微的修改。
总结
其实跟着官方的教程一步步来集成还是很简单的,十分、八分钟吧。在使用后觉得反应是真的快,比之前使用的 Algolia 快太多了,而且还方便,Algolia 在hugo
之后还得输入npm run algolia
,fastsearch 不需任何命令,正常的hugo
就行。