给 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,欢迎访问订阅.个人博客小站刚建站不久,想着除了主题里的功能外再添加上相册模块,于是半搜索半摸索把相册模块搞出来了,最后采用了利用腾讯云对象存储作图 ...
随机推荐
- vue2与vue3实现响应式的原理区别和提升
区别: vue2.x: 实现原理: 对象类型:Object.defineProperty()对属性的读取,修改进行拦截(数据劫持): 数组类型:通过重写更新数组的一系列方法来进行拦截(对数组的变更方法 ...
- 活动报名|对话贡献者:DolphinScheduler x Pulsar 在线 Meetup
各位 DolphinScheduler 和 Pulsar 社区的小伙伴们,Apache DolphinScheduler x Pulsar 在线 Meetup 来啦! 导语 大数据任务调度.消息流的订 ...
- Redis 02 数据库
参考源 https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0 版本 本文章基于 Redis 6.2.6 Redis 默 ...
- ZZH与计数(矩阵加速,动态规划,记忆化搜索)
题面 因为出题人水平很高,所以这场比赛的题水平都很高. ZZH 喜欢计数. ZZH 有很多的数,经过统计,ZZH一共有 v 0 v_0 v0 个 0 , v 1 v_1 v1 个 1,-, v 2 ...
- Clickhouse基准测试实践
1.概述 本篇博客将对MySQL.InfluxDB.Clickhouse在写入时间.聚合查询时间.磁盘使用等方面的性能指标来进行比较. 2.内容 比较的数据集,是使用的Clickhouse官网提供的6 ...
- 字节跳动端智能工程链路 Pitaya 的架构设计
Client AI 是字节跳动产研架构下属的端智能团队,负责端智能 AI 框架和平台的建设,也负责模型和算法的研发,为字节跳动开拓端上智能新场景.本文介绍的 Pitaya 是由字节跳动的 Client ...
- Linux的NIS配置
快速命令 # Server和Client设置NIS域名 nisdomainname nis_server echo 'NISDOMAIN=nis_server' >> /etc/sysco ...
- C语言:多功能计算器 (矩阵相乘)
好家伙,实现矩阵相乘功能 代码如下: void fifth()//矩阵的相乘// { int a[100][100],b[100][100]; int d,e,f,h,j,k,t; double su ...
- 第三十三篇:关于ES6,JSON和Webpack
好家伙 1.什么是ES6? ECMAScript是javascript标准 ES6就是ECMAScript的第6个版本 (大概是一个语法标准规范) 2.什么是JSON? JSON 是什么,在数据交换中 ...
- Windows平台摄像头或屏幕RTMP推送介绍:OBS VS SmartPublisher
好多开发者问道,既然有了OBS,你们为什么还要开发SmartPublisher? 的确,在我们进行Windows平台RTMP推送模块开发之前,市面上为数不多的Windows平台RTMP推流工具当属OB ...