给 hugo 博客添加搜索功能

起因我的博客使用了 hugo 作为静态生成工具,自带的主题里也没有附带搜索功能 。看来,还是得自己给博客添加一个搜索功能 。
经过多方查找,从 Hugo Fast Search · GitHub 找到一片详细、可用的教程(虽然后面魔改了一些) 。
实际案例步骤

  1. 在 config.toml 文件做好相关配置;
  2. 添加导出 JSON 格式文件的脚本,即在 layouts/_default 目录下添加 index.json 文件;
  3. 增加依赖的 JS 脚本 , 包含自己的 search.js 和 fuse.js 文件;
  4. 添加相关 HTML 代码;
  5. 添加相关 CSS 样式 。
配置[params]# 是否开启本地搜索fastSearch = true[outputs]# 增加 JSON 配置home = ["HTML", "RSS", "JSON"]添加 index.json 文件{{- $.Scratch.Add "index" slice -}}{{- range .Site.RegularPages -}}{{- $.Scratch.Add "index" (dict "title" .Title "permalink" .Permalink "content" .Plain) -}}{{- end -}}{{- $.Scratch.Get "index" | jsonify -}}添加依赖首先,可以先添加 fuse.js 依赖,它是一个功能强大的轻量级模糊搜索库,可以到 官网 访问更多信息:
{{- if .Site.Params.fastSearch -}}<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.4.6"></script>{{- end -}}然后,就是添加自定义的 search.js 文件以实现搜索功能,文件放置在 assets/js 目录下 。
这里的代码和 Gist 上的有些许不同 , 经过了自己的魔改 。
var fuse; // holds our search enginevar 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("click", event => {var cDom = document.getElementById("fastSearch");var sDom = document.getElementById('search-click');var tDom = event.target;if (sDom == tDom || sDom.contains(tDom)) {showSearchInput();} else if (cDom == tDom || cDom.contains(tDom)) {// ...} else if (searchVisible) {cDom.style.display = "none"searchVisible = false;}});document.addEventListener('keydown', function(event) {// CMD-/ to show / hide Searchif (event.metaKey && event.which === 191) {showSearchInput()}// Allow ESC (27) to close search boxif (event.keyCode == 27) {if (searchVisible) {document.getElementById("fastSearch").style.display = "none";document.activeElement.blur();searchVisible = false;}}// DOWN (40) arrowif (event.keyCode == 40) {if (searchVisible && resultsAvailable) {event.preventDefault(); // stop window from scrollingif ( 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 thereelse { document.activeElement.parentElement.nextSibling.firstElementChild.focus(); } // otherwise select the next search result}}// UP (38) arrowif (event.keyCode == 38) {if (searchVisible && resultsAvailable) {event.preventDefault(); // stop window from scrollingif ( document.activeElement == maininput) { maininput.focus(); } // If we're in the input box, do nothingelse if ( document.activeElement == first) { maininput.focus(); } // If we're at the first item, go to input boxelse { 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);}function showSearchInput() {// 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 neededif(firstRun) {loadSearch(); // loads our json data and builds fuse.js search indexfirstRun = false; // let's never do this again}// Toggle visibility of search boxif (!searchVisible) {document.getElementById("fastSearch").style.display = "block"; // show search boxdocument.getElementById("searchInput").focus(); // put focus in input box so you can just start typingsearchVisible = true; // search visible}else {document.getElementById("fastSearch").style.display = "none"; // hide search boxdocument.activeElement.blur(); // remove focus from search boxsearchVisible = false; // search not visible}}// ==========================================// 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 = https://www.huyubaike.com/biancheng/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() {fetchJSONFile('/index.json', function(data){var options = { // fuse.js options; check fuse.js website for detailsincludeMatches: true,shouldSort: true,ignoreLocation: true,keys: [{name: 'title',weight: 1,},{name: 'content',weight: 0.6,},],};fuse = new Fuse(data, options); // 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) {if (term.length == 0) {document.getElementById("searchResults").setAttribute("style", "");return;}let results = fuse.search(term); // the actual query being run using fuse.jslet searchItems = ''; // our results bucketif (results.length === 0) { // no results based on what was typed into the input boxresultsAvailable = false;searchItems = '<li class="noSearchResult">无结果</li>';} else { // build our htmlpermalinkList = []searchItemCount = 0for (let item in results) {if (permalinkList.includes(results[item].item.permalink)) {continue;}// 去重permalinkList.push(results[item].item.permalink);searchItemCount += 1;title = results[item].item.title;content = results[item].item.content.slice(0, 50);for (const match of results[item].matches) {if (match.key == 'title') {startIndex = match.indices[0][0];endIndex = match.indices[0][1] + 1;highText = '<span class="search-highlight">' + match.value.slice(startIndex, endIndex) + '</span>';title = match.value.slice(0, startIndex) + highText + match.value.slice(endIndex);} else if (match.key == 'content') {startIndex = match.indices[0][0];endIndex = match.indices[0][1] + 1;highText = '<span class="search-highlight">' + match.value.slice(startIndex, endIndex) + '</span>';content = match.value.slice(Math.max(0, startIndex - 30), startIndex) + highText + match.value.slice(endIndex, endIndex + 30);}}searchItems = searchItems + '<li><a href="' + results[item].item.permalink + '">' + '<span class="title">' + title + '</span><br /> <span class="sc">'+ content +'</span></a></li>';// only show first 5 resultsif (searchItemCount >= 5) {break;}}resultsAvailable = true;}document.getElementById("searchResults").setAttribute("style", "display: block;");document.getElementById("searchResults").innerHTML = searchItems;if (results.length > 0) {first = list.firstChild.firstElementChild; // first result container — used for checking against keyboard up/down locationlast = list.lastChild.firstElementChild; // last result container — used for checking against keyboard up/down location}}

推荐阅读