给 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 engine 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("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 Search   if (event.metaKey && event.which === 191) {       showSearchInput()   }    // Allow ESC (27) to close search box   if (event.keyCode == 27) {     if (searchVisible) {       document.getElementById("fastSearch").style.display = "none";       document.activeElement.blur();       searchVisible = false;     }   }    // DOWN (40) arrow   if (event.keyCode == 40) {     if (searchVisible && resultsAvailable) {       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); }  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 needed   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) {     document.getElementById("fastSearch").style.display = "block"; // show search box     document.getElementById("searchInput").focus(); // put focus in input box so you can just start typing     searchVisible = true; // search visible   }   else {     document.getElementById("fastSearch").style.display = "none"; // hide search box     document.activeElement.blur(); // remove focus from search box     searchVisible = 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 = 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 details       includeMatches: 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.js   let searchItems = ''; // our results bucket    if (results.length === 0) { // no results based on what was typed into the input box     resultsAvailable = false;     searchItems = '<li class="noSearchResult">无结果</li>';   } else { // build our html     permalinkList = []     searchItemCount = 0     for (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 results       if (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 location     last = list.lastChild.firstElementChild; // last result container — used for checking against keyboard up/down location   } } 

最后,需要将 search.js 依赖引入,如下是引入的代码:

{{ $search := resources.Get "js/search.js" | minify | fingerprint }} <script type="text/javascript" src="{{ $search.RelPermalink }}"></script> 

添加 HTML 代码

HTML 页面的代码分为两个部分:搜索的按钮、搜索框和结果展示。

我这里将搜索的按钮放到的菜单栏,主要是一个可点击的按钮:

{{ if .Site.Params.fastSearch -}} <li id="search-click" class="menu-item">     <a class="menu-item-link" href="javascript:void(0)">搜索</a> </li> {{- end }} 

对于搜索框,我选择的是弹出式的窗口,这里比较重要的是标签的 ID 需要和 search.js 脚本一致:

{{ if .Site.Params.fastSearch -}} <div id="fastSearch">     <input id="searchInput">     <ul id="searchResults"></ul> </div> {{- end }} 

添加 CSS 样式

页面样式这部分,主要是看个人的喜好,这里只放出自己的样式:

#fastSearch {     display: none;     position: fixed;     left: 50%;     top: calc(5vw + 40px);     transform: translateX(-50%);     z-index: 4;     width: 650px;     background-color: #fff;     box-shadow: 0 1px 2px #3c40434d, 0 2px 6px 2px #3c404326;     border-radius: 4px;     overflow: hidden;      input {         padding: 10px;         width: 100%;         height: 30px;         font-size: 18px;         line-height: 30px;         border: none;         outline: none;         font-family: inherit;     }      #searchResults {         display: none;         overflow-y: auto;         max-height: 60vh;         padding-left: 0;         margin: 0;         border-top: 1px dashed #ddd;          .search-highlight {             color: red;         }          li {             list-style: none;             margin: 0;              a {                 text-decoration: none;                 color: inherit;                 padding: 6px 10px;                 display: block;                 font-size: 14px;                 letter-spacing: .04em;             }              a:hover,             a:focus {                 filter: brightness(93%);                 outline: 0;                 background-color: rgb(240, 240, 240);             }              .title {                 font-weight: 600;             }         }          li.noSearchResult {             text-align: center;             margin: 8px 0;             color: #888;         }     } } 

样例展示

给 hugo 博客添加搜索功能

总结

经过两天时间的奋斗,终于是将搜索功能给上线了。

不得不说,理想总是一开始美好,最初以为是一个完整、可用的教程,却没想到复制到代码之后就不可用了,最终是经过自己的魔改才得以使用。

总结一下就是,没有实践就没有话语权,千万不要做管中窥豹的那个人。

发表评论

评论已关闭。

相关文章