为Hugo博客添加搜索功能

来自 老麦
862 浏览

起因

一直以来我对博客的功能要求都不高,比如说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: .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就行。

您也许会喜欢……

欢迎留言来分享您的观点

18 评论

Andy烧麦 2021-08-10 - 09:30

用JANE测试,按这个方法没能成功,不知道问题出在哪里?

回复
老麦 2021-08-10 - 12:31

有没有修改config.toml这个配置文件?[outputs]

回复
Andy烧麦 2021-08-10 - 14:49

有,都是按操作来的 [outputs]
home = [“HTML”, “RSS”, “JSON”]

回复
老麦 2021-08-10 - 18:05

你是用Hugo Extended吗?要用这个版本才行哦。

回复
ANDY烧麦 2021-08-03 - 21:46

要给你点赞,下班路上,在滴滴上,看懂了

回复
老麦 2021-08-03 - 21:49

为什么你要研究这个啊?难道你要转到静态去?

回复
Andy烧麦 2021-08-04 - 11:33

是有这么个想法,在线时间不够多 ,还是Typora、语雀用得最多,那么 MD 直接定期导,是不是更方便?

回复
老麦 2021-08-04 - 12:58

是的,可以这么说吧?各有各的好处,hugo不能多端和在线更新,也有麻烦。虽然可以利用github actions可以实现在线,但用起来也不方便。不过你用typora多的话,转静态有优势,而且以后花费的成本相对要低。除域名这笔消费外可以做到免费。

回复
Andy烧麦 2021-08-04 - 13:39

多是电脑,手机和PAD基本不会更新,但是OneDrive是能同步X1+MBP+MINI,也就一定程度能实现多端把。主机刚付了1年的费用,到无所谓,留着后悔也行。

老麦 2021-08-04 - 18:02

这个就要看你自己咯,wordpress和静态使用起来都差不多。不过要我最终选一个的话,我也会用回静态,毕竟小博客,静态足够了。但这个等我服务器到期再作考虑咯。

Andy烧麦 2021-08-04 - 18:47

虽然现在是Github,未来也可能是对象存储来放静态博客啊。

老麦 2021-08-04 - 19:28

嗯,可以有,毕竟github在国内速度还是有点慢的。

Andy烧麦 2021-08-05 - 12:35

Github静态连外链都很少,被百度收入上百个,而动态的,一直70-80,还经常被K

回复
老麦 2021-08-05 - 17:59

哈哈哈,如果你在乎这个那就直接用对象储存好了。

回复
wu先生 2021-04-08 - 21:42

哈哈,一般来说用这些非主流的本意是不折腾,但到头来,折腾得更多。

回复
老麦 2021-04-08 - 23:51

哈哈哈,是这么个理。但有时候就是受不住诱惑,有一颗爱折腾的心。

回复
飞牛士 2021-04-01 - 19:28

折腾一下,锻炼学习能力和执行力。

回复
老麦 2021-04-01 - 20:03

哈哈哈,生命在于折腾,不是在折腾,就是在赶往折腾的路上。

回复