昨晚我走在路上的时候突然想到,随着时间推移,我喜欢的作品一定会变得越来越多,而我的藏书阁里目前仅使用了Tags和Categories的树状分类法,就算是个自留地,日后在博客内指名寻找某个作品一定是耗时的。如何解决这个问题,最佳方案就是为博客添加搜索功能。
最开始我尝试的方案是安装搜索插件hugo-search-fuse-js。这是一个基于Fuse.js
的主题插件,但不知为什么,search页面没有显示任何内容。正当我打算放弃的时候,我又发现了另一个简洁高效的搜索方案:Hugo Fast Search,不需要注册登录,没有抓取次数限制,最小依赖无需编译。
参考教程
这位博主对搜索功能进行了客制化魔改:
- 允许通过点击页面空白处隐藏搜索框,而不是只能按Esc
- 在右上角添加了一个搜索按钮,方便不想按快捷键的人
- 默认的快捷键由于Firefox Linux默认
Super-/
是 Quick Find 功能,因此改成了Alt-/
开始配置
添加index.json
我使用的主题是已经被作者弃坑的Mogege,首先添加index.json
文件到layouts/_default
,内容如下:
{{- $.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.js
和fuse.min.js
到static/js
。fuse.min.js
可从Releases页面下载source code后解压进入dist文件夹就能找到。fastsearch.js
内容如下:
var fuse; // holds our search enginevar fuseIndex;var searchVisible = false;var firstRun = true; // allow us to delay loading json data unless search activatedvar list = document.getElementById('searchResults'); // targets the <ul>var first = list.firstChild; // first child of search listvar last = list.lastChild; // last child of search listvar maininput = document.getElementById('searchInput'); // input box for searchvar 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.css
或main.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
就可以在本地预览效果了。