为Hugo博客添加搜索功能

来自 老麦
405 浏览

起因

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

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

回复