起因

我的博客使用了 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 博客添加搜索功能的更多相关文章

  1. 给jekyll博客添加搜索功能

    使用SWIFTYPE为jekyll博客添加搜索引擎 步骤 1.首先去swiftype注册一个账号 2.接着添加自己想要配置的网站地址并为新设定的引擎添加一个名字(非会员只能设置一个引擎). 3.收到验 ...

  2. hexo next主题为博客添加分享功能

    title: hexo next主题为博客添加分享功能 date: 2018-01-06 20:20:02 tags: [hexo博客, 博客配置] categories: hexo next主题配置 ...

  3. 为CSDN博客添加打赏功能

    随着移动支付在国内的兴起,越来越多的付费内容越多如雨后春笋般的冒了出来.其中以<逻辑思维>.罗振宇.李笑来为主要代表作品和人物. 现在很多博客或者个人网站里面都有打赏功能,这算是对博主的劳 ...

  4. 【hugo】- hugo 博客 添加鼠标单击特效

    hugo 博客 监听鼠标点击事件,添加动画效果 js下载 链接:https://pan.baidu.com/s/1SZu76WdEXRxLCfqJ2lbbtQ 密码:r056 移入hugo博客中 打开 ...

  5. hexo+next博客添加搜索

    1.为什么添加algolia搜索 第一当然是可以方便的查找所需文章,第二点就是之前常用的swiftype插件不再免费.我的个人博客是这个月初搭建完成的,这时候swiftype已经不再免费,而且只开放企 ...

  6. 使用Hugo框架搭建博客的过程 - 功能拓展

    前言 本文介绍一些拓展功能,如文章页面功能增加二级菜单,相关文章推荐和赞赏.另外,使用脚本会大大简化写作后的上传流程. 文章页面功能 这部分功能的拓展主要是用前端的JS和CSS,如果对前端不了解,可以 ...

  7. 【干货】2个小时教你hexo博客添加评论、打赏、RSS等功能 (转)

    备注:该教程基于Hexo 2.x版本,目前Hexo是3.x版本,照本教程实现有可能会出现404错误,笔者目前还未找时间去解决,待笔者找时间解决该问题后,再写一篇该问题的解决教程,给各位读者带来困扰,还 ...

  8. hexo博客添加功能

    设置Hexo主题模式 Hexo主题中,有三种不同的模式,通过切换模式,让NexT主题显示不一样的样式.在NexT根目录下有一个同样名称为_config.yml,为了区分hexo根目录下的_config ...

  9. 如何利用腾讯云COS为静态博客添加动态相册

    前言 本文首发于个人网站Jianger's Blog,欢迎访问订阅.个人博客小站刚建站不久,想着除了主题里的功能外再添加上相册模块,于是半搜索半摸索把相册模块搞出来了,最后采用了利用腾讯云对象存储作图 ...

随机推荐

  1. mybatis 04: mybatis对象分析 + 测试代码简化 + 配置优化

    MyBatis对象分析 测试代码示例 package com.example.test; import com.example.pojo.Student; import org.apache.ibat ...

  2. Apache DolphinScheduler&ShenYu(Incubating) 联合 Meetup,暖春 3 月与你相约!

    云霞出海曙,梅柳渡江春. 2022 年的早春在疫情中显得格外生机勃勃,虽然接下来寒流仍有可能造访国内部分地区,但开源的世界,早已热闹非凡! 2022 年 3 月 26 日(星期六), Apache D ...

  3. 深入理解Spring事件机制(一):广播器与监听器的初始化

    前言 Spring 从 3.x 开始支持事件机制.在 Spring 的事件机制中,我们可以令一个事件类继承 ApplicationEvent 类,然后将实现了 ApplicationListener ...

  4. 从零搭建云原生技术kubernetes(K8S)环境-通过kubesPhere的AllInOne方式

    前言 k8s云原生搭建,步骤有点多,但通过kubesphere,可以快速搭建k8s环境,同时有一个以 Kubernetes 为内核的云原生分布式操作系统-kubesphere,本文将从零开始进行kub ...

  5. HTML初学者小知识2

    网页内嵌 代码以及演示如下 代码 <div id="tab_1"> <iframe src="div.html" height="5 ...

  6. 做自动化测试选择Python还是Java?

    你好,我是测试蔡坨坨. 今天,我们来聊一聊测试人员想要进阶,想要做自动化测试,甚至测试开发,如何选择编程语言. 前言 自动化测试,这几年行业内的热词,也是测试人员进阶的必备技能,更是软件测试未来发展的 ...

  7. 避免jquery多次监听事件

    jQuery.event.dispatch 事件分发监听源码简单理解是将绑定的事件放入队列后进行监听,如果对一个事件多次绑定(on或者bind),事件会重复添加到队列等待jq监听,这样会导致很大资源消 ...

  8. python 二分法查找字典中指定项第一次出现的索引

    import time #引入time库,后续计算时间. inform_m = {} #创建母字典 inform_s = {} #母字典下嵌套的子字典 #给母字典添加键-值 for i in rang ...

  9. java 类名后加变量名是什么意思?

    回答这个问题我们需要先了解两个事情: A是一个类,我们如果对他进行实例化,需要这样写: A a = new A(); 详细解释一下这个语句,首先等号左边做的事情:在JVM栈内存(stack)中定义了一 ...

  10. MySQL建表语句生成Golang代码

    1. 背景 对于后台开发新的需求时,一般会先进行各种表的设计,写各个表的建表语句 然后根据建立的表,写对应的model代码.基础的增删改查代码(基础的增删改查服务可以划入DAO(Data Access ...