给 hugo 博客添加搜索功能
起因
我的博客使用了 hugo 作为静态生成工具,自带的主题里也没有附带搜索功能。看来,还是得自己给博客添加一个搜索功能。
经过多方查找,从 Hugo Fast Search · GitHub 找到一片详细、可用的教程(虽然后面魔改了一些)。
实际案例
步骤
- 在 config.toml 文件做好相关配置;
- 添加导出 JSON 格式文件的脚本,即在 layouts/_default目录下添加 index.json 文件;
- 增加依赖的 JS 脚本,包含自己的 search.js 和 fuse.js 文件;
- 添加相关 HTML 代码;
- 添加相关 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 博客添加搜索功能的更多相关文章
- 给jekyll博客添加搜索功能
		使用SWIFTYPE为jekyll博客添加搜索引擎 步骤 1.首先去swiftype注册一个账号 2.接着添加自己想要配置的网站地址并为新设定的引擎添加一个名字(非会员只能设置一个引擎). 3.收到验 ... 
- hexo next主题为博客添加分享功能
		title: hexo next主题为博客添加分享功能 date: 2018-01-06 20:20:02 tags: [hexo博客, 博客配置] categories: hexo next主题配置 ... 
- 为CSDN博客添加打赏功能
		随着移动支付在国内的兴起,越来越多的付费内容越多如雨后春笋般的冒了出来.其中以<逻辑思维>.罗振宇.李笑来为主要代表作品和人物. 现在很多博客或者个人网站里面都有打赏功能,这算是对博主的劳 ... 
- 【hugo】- hugo 博客 添加鼠标单击特效
		hugo 博客 监听鼠标点击事件,添加动画效果 js下载 链接:https://pan.baidu.com/s/1SZu76WdEXRxLCfqJ2lbbtQ 密码:r056 移入hugo博客中 打开 ... 
- hexo+next博客添加搜索
		1.为什么添加algolia搜索 第一当然是可以方便的查找所需文章,第二点就是之前常用的swiftype插件不再免费.我的个人博客是这个月初搭建完成的,这时候swiftype已经不再免费,而且只开放企 ... 
- 使用Hugo框架搭建博客的过程 - 功能拓展
		前言 本文介绍一些拓展功能,如文章页面功能增加二级菜单,相关文章推荐和赞赏.另外,使用脚本会大大简化写作后的上传流程. 文章页面功能 这部分功能的拓展主要是用前端的JS和CSS,如果对前端不了解,可以 ... 
- 【干货】2个小时教你hexo博客添加评论、打赏、RSS等功能  (转)
		备注:该教程基于Hexo 2.x版本,目前Hexo是3.x版本,照本教程实现有可能会出现404错误,笔者目前还未找时间去解决,待笔者找时间解决该问题后,再写一篇该问题的解决教程,给各位读者带来困扰,还 ... 
- hexo博客添加功能
		设置Hexo主题模式 Hexo主题中,有三种不同的模式,通过切换模式,让NexT主题显示不一样的样式.在NexT根目录下有一个同样名称为_config.yml,为了区分hexo根目录下的_config ... 
- 如何利用腾讯云COS为静态博客添加动态相册
		前言 本文首发于个人网站Jianger's Blog,欢迎访问订阅.个人博客小站刚建站不久,想着除了主题里的功能外再添加上相册模块,于是半搜索半摸索把相册模块搞出来了,最后采用了利用腾讯云对象存储作图 ... 
随机推荐
- BZOJ3224/LuoguP3369 普通平衡树 (splay)
			终末のcode #include <iostream> #include <cstdio> #include <cstring> #include <algo ... 
- 深入分析FragmentPagerAdapter和FragmentStatePagerAdapter
			最近遇到比较奇怪的bug,TableLayout+ViewPager实现点击顶部tab切换viewpager视图.但是在Viewpager设置dapter时,最开始设置的是FragmentPagerA ... 
- Java反射(重要)
			全文内容 1: 获取字节码文件对象的三种方式 2: 获取公有,私有方法,并调用构造方法,成员方法 3: 获取并调用私有成员变量 4: 如何为实例对象的成员变量赋值 5: 文末有一些注意 tea1类代码 ... 
- 牛客CSP-S模拟题——十二桥问题
			题面 n <= 50000,m <= 200000,k <= 12 题解 可以从K条边的两端和1结点出发各进行一次O(nlogn)的Dijk,然后就浓缩成了一个最多只有25个点的小完 ... 
- java基础———打印三角形
			代码 public static void main(String[] args) { for (int i = 1; i <= 5; i++) { for (int j = 5; j > ... 
- vue-router4 |name的作用|query传参|parmas传参|动态路由参数|命名视图|别名alias|前置路由守卫|路由过渡效果|滚动行为
			vue-router4 出现 No match found for location with path "/" #### router/index.ts文件 import { c ... 
- 报错:①Tog goal specified requires a project to execute but there is no POM in this directory......②说类HelloWorld是公共的, 应在名为 HelloWorld.java 的文件中声明 public class HelloWorld......
			在运行Maven的命令时,在DOS窗口里面必须把目录切换到项目的根部,要不然命令是找不到目的地. 下图是错误示范,项目在Demo02这个目录里,就必须将目录切换到Demo02下,否则DOS窗口只有飘红 ... 
- django_day03
			django_day03 Django的view(视图) CBV和FBV FBV:function based view 基于函数的视图 CBV:class based view 基于类的视图 fro ... 
- gem5 使用记录,对例子中helloobject的理解
			gem5中有一个 hello的例子,不是hello world那个,在src/learning-gem5/part2里面,这是虽然是个简单的例子但包含的要素挺多挺全. 整个结构是src下面有一个hel ... 
- JAVA中方法的调用主要有以下几种
			JAVA中方法的调用主要有以下几种: 1.非静态方法 非静态方法就是没有 static 修饰的方法,对于非静态方法的调用,是通过对 象来调用的,表现形式如下. 对象名.方法() eg: public ... 
