1695 words
8 minutes
Hugo | 为Mogege主题添加搜索功能

昨晚我走在路上的时候突然想到,随着时间推移,我喜欢的作品一定会变得越来越多,而我的藏书阁里目前仅使用了Tags和Categories的树状分类法,就算是个自留地,日后在博客内指名寻找某个作品一定是耗时的。如何解决这个问题,最佳方案就是为博客添加搜索功能。

最开始我尝试的方案是安装搜索插件hugo-search-fuse-js。这是一个基于Fuse.js的主题插件,但不知为什么,search页面没有显示任何内容。正当我打算放弃的时候,我又发现了另一个简洁高效的搜索方案:Hugo Fast Search,不需要注册登录,没有抓取次数限制,最小依赖无需编译。

参考教程

5分钟给Hugo博客增加搜索功能

这位博主对搜索功能进行了客制化魔改:

  • 允许通过点击页面空白处隐藏搜索框,而不是只能按Esc
  • 在右上角添加了一个搜索按钮,方便不想按快捷键的人
  • 默认的快捷键由于Firefox Linux默认 Super-/ 是 Quick Find 功能,因此改成了 Alt-/

开始配置#

添加index.json#

我使用的主题是已经被作者弃坑的Mogege,首先添加index.json文件到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文件#

添加fastsearch.jsfuse.min.jsstatic/jsfuse.min.js可从Releases页面下载source code后解压进入dist文件夹就能找到。fastsearch.js内容如下:

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,也可以在layouts/_default/baseof.html添加:

<!-- 在menu-item代码段下(对应博客菜单栏后)添加-->
<a id="search-btn" style="display: inline-block;" href="javascript:void(0);">
<i class="fas fa-angle-down"></i>
</a>
<div id="fastSearch">
<input id="searchInput" tabindex="0">
<ul id="searchResults">
</ul>
</div>

i标签调用了免费的Font Awesome图标。如果没有用过这套图标请看这篇博客。我在这里用的图标是下标(angle-down),效果请去这里查看。

当然你也可以使用自己喜欢的图标,我试用过搜索(search),羽毛笔(feather-alt),纸飞机(paper-plane),左引号(quote-left)和对话框(discourse)都还不错,甚至把调用图标这一动作改为插入文字都没问题,将<i class="fas fa-iconname"></i>改为<span>这里填写你想显示的文字</span>,例如<span>search</span>即可。

在主题模板上引用js#

我使用的主题有一个专门引用js的模板,所以我选择在此添加引用。在/layouts/partials/js.html(有的主题是/layouts/partials/scripts.html)添加:

<!-- Fastsearch -->
<script src="/js/fuse.min.js"></script>
<script src="/js/fastsearch.js"></script>

添加CSS样式#

CSS样式来自老麦,我喜欢这个颜色,自认为与Mogege主题很合,没有进行改动。

尽量选择对应的模板来添加,比如说我是在header里修改的,那么我在/assets/css/_common/_partial/header.scss添加CSS样式。也可以修改模板的主CSS文件,通常是style.cssmain.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: .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;
}

可以自己稍作调整。

配置好后,运行hugo server -D就可以在本地预览效果了。

Hugo | 为Mogege主题添加搜索功能
https://blog.tantalum.life/posts/hugo-fast-search/
Author
Zokiio
Published at
2021-11-22
License
CC BY-NC-SA 4.0