随着博客的文章越来越多,原本没有搜索功能的博客变得难以使用起来,博客添加一个搜索功能势在必行啊。
起因
我的博客使用了 hugo 作为静态生成工具,自带的主题里也没有附带搜索功能。看来,还是得自己给博客添加一个搜索功能。
经过多方查找,从 Hugo Fast Search · GitHub 找到一片详细、可用的教程(虽然后面魔改了一些)。
实际案例
步骤
- 在 config.toml 文件做好相关配置;
- 添加导出 JSON 格式文件的脚本,即在
layouts/_default
目录下添加 index.json 文件; - 增加依赖的 JS 脚本,包含自己的 search.js 和 fuse.js 文件;
- 添加相关 HTML 代码;
- 添加相关 CSS 样式。
配置
1
2
3
4
5
6
7
| [params]
# 是否开启本地搜索
fastSearch = true
[outputs]
# 增加 JSON 配置
home = ["HTML", "RSS", "JSON"]
|
添加 index.json 文件
1
2
3
4
5
| {{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
{{- $.Scratch.Add "index" (dict "title" .Title "permalink" .Permalink "content" .Plain) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
|
添加依赖
首先,可以先添加 fuse.js 依赖,它是一个功能强大的轻量级模糊搜索库,可以到 官网 访问更多信息:
1
2
3
| {{- if .Site.Params.fastSearch -}}
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.4.6"></script>
{{- end -}}
|
然后,就是添加自定义的 search.js 文件以实现搜索功能,文件放置在 assets/js
目录下。
这里的代码和 Gist 上的有些许不同,经过了自己的魔改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
| 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 依赖引入,如下是引入的代码:
1
2
| {{ $search := resources.Get "js/search.js" | minify | fingerprint }}
<script type="text/javascript" src="{{ $search.RelPermalink }}"></script>
|
添加 HTML 代码
HTML 页面的代码分为两个部分:搜索的按钮、搜索框和结果展示。
我这里将搜索的按钮放到的菜单栏,主要是一个可点击的按钮:
1
2
3
4
5
| {{ 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 脚本一致:
1
2
3
4
5
6
| {{ if .Site.Params.fastSearch -}}
<div id="fastSearch">
<input id="searchInput">
<ul id="searchResults"></ul>
</div>
{{- end }}
|
添加 CSS 样式
页面样式这部分,主要是看个人的喜好,这里只放出自己的样式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| #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;
}
}
}
|
样例展示

总结
经过两天时间的奋斗,终于是将搜索功能给上线了。
不得不说,理想总是一开始美好,最初以为是一个完整、可用的教程,却没想到复制到代码之后就不可用了,最终是经过自己的魔改才得以使用。
总结一下就是,没有实践就没有话语权,千万不要做管中窥豹的那个人。