为Hugo博客添加搜索功能
起因
一直以来我对博客的功能要求都不高,比如说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,内容为
1{{- $.Scratch.Add "index" slice -}}
2{{- range .Site.RegularPages -}}
3 {{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "categories" .Params.categories "contents" .Plain "permalink" .Permalink "date" .Date "section" .Section) -}}
4{{- end -}}
5{{- $.Scratch.Get "index" | jsonify -}}
修改配置文件config.toml
1[outputs]
2 home = ["HTML", "RSS", "JSON"]
添加js文件
在Hugo默认的静态文件目录/static/js/添加fastsearch.js和fuse.js。fuse.js这里建议使用官方提供fuse.min.js,下载地址:https://github.com/krisk/fuse
fastsearch.js内容如下
1var fuse; // holds our search engine
2var fuseIndex;
3var searchVisible = false;
4var firstRun = true; // allow us to delay loading json data unless search activated
5var list = document.getElementById('searchResults'); // targets the <ul>
6var first = list.firstChild; // first child of search list
7var last = list.lastChild; // last child of search list
8var maininput = document.getElementById('searchInput'); // input box for search
9var resultsAvailable = false; // Did we get any search results?
10
11// ==========================================
12// The main keyboard event listener running the show
13//
14document.addEventListener('keydown', function(event) {
15
16 // CMD-/ to show / hide Search
17 if (event.altKey && event.which === 191) {
18 // Load json search index if first time invoking search
19 // Means we don't load json unless searches are going to happen; keep user payload small unless needed
20 doSearch(event)
21 }
22
23 // Allow ESC (27) to close search box
24 if (event.keyCode == 27) {
25 if (searchVisible) {
26 document.getElementById("fastSearch").style.visibility = "hidden";
27 document.activeElement.blur();
28 searchVisible = false;
29 }
30 }
31
32 // DOWN (40) arrow
33 if (event.keyCode == 40) {
34 if (searchVisible && resultsAvailable) {
35 console.log("down");
36 event.preventDefault(); // stop window from scrolling
37 if ( document.activeElement == maininput) { first.focus(); } // if the currently focused element is the main input --> focus the first <li>
38 else if ( document.activeElement == last ) { last.focus(); } // if we're at the bottom, stay there
39 else { document.activeElement.parentElement.nextSibling.firstElementChild.focus(); } // otherwise select the next search result
40 }
41 }
42
43 // UP (38) arrow
44 if (event.keyCode == 38) {
45 if (searchVisible && resultsAvailable) {
46 event.preventDefault(); // stop window from scrolling
47 if ( document.activeElement == maininput) { maininput.focus(); } // If we're in the input box, do nothing
48 else if ( document.activeElement == first) { maininput.focus(); } // If we're at the first item, go to input box
49 else { document.activeElement.parentElement.previousSibling.firstElementChild.focus(); } // Otherwise, select the search result above the current active one
50 }
51 }
52});
53
54
55// ==========================================
56// execute search as each character is typed
57//
58document.getElementById("searchInput").onkeyup = function(e) {
59 executeSearch(this.value);
60}
61
62document.querySelector("body").onclick = function(e) {
63 if (e.target.tagName === 'BODY' || e.target.tagName === 'DIV') {
64 hideSearch()
65 }
66}
67
68document.querySelector("#search-btn").onclick = function(e) {
69 doSearch(e)
70}
71
72function doSearch(e) {
73 e.stopPropagation();
74 if (firstRun) {
75 loadSearch() // loads our json data and builds fuse.js search index
76 firstRun = false // let's never do this again
77 }
78 // Toggle visibility of search box
79 if (!searchVisible) {
80 showSearch() // search visible
81 }
82 else {
83 hideSearch()
84 }
85}
86
87function hideSearch() {
88 document.getElementById("fastSearch").style.visibility = "hidden" // hide search box
89 document.activeElement.blur() // remove focus from search box
90 searchVisible = false
91}
92
93function showSearch() {
94 document.getElementById("fastSearch").style.visibility = "visible" // show search box
95 document.getElementById("searchInput").focus() // put focus in input box so you can just start typing
96 searchVisible = true
97}
98
99// ==========================================
100// fetch some json without jquery
101//
102function fetchJSONFile(path, callback) {
103 var httpRequest = new XMLHttpRequest();
104 httpRequest.onreadystatechange = function() {
105 if (httpRequest.readyState === 4) {
106 if (httpRequest.status === 200) {
107 var data = JSON.parse(httpRequest.responseText);
108 if (callback) callback(data);
109 }
110 }
111 };
112 httpRequest.open('GET', path);
113 httpRequest.send();
114}
115
116
117// ==========================================
118// load our search index, only executed once
119// on first call of search box (CMD-/)
120//
121function loadSearch() {
122 console.log('loadSearch()')
123 fetchJSONFile('/index.json', function(data){
124
125 var options = { // fuse.js options; check fuse.js website for details
126 shouldSort: true,
127 location: 0,
128 distance: 100,
129 threshold: 0.4,
130 minMatchCharLength: 2,
131 keys: [
132 'permalink',
133 'title',
134 'tags',
135 'contents'
136 ]
137 };
138 // Create the Fuse index
139 fuseIndex = Fuse.createIndex(options.keys, data)
140 fuse = new Fuse(data, options, fuseIndex); // build the index from the json file
141 });
142}
143
144
145// ==========================================
146// using the index we loaded on CMD-/, run
147// a search query (for "term") every time a letter is typed
148// in the search box
149//
150function executeSearch(term) {
151 let results = fuse.search(term); // the actual query being run using fuse.js
152 let searchitems = ''; // our results bucket
153
154 if (results.length === 0) { // no results based on what was typed into the input box
155 resultsAvailable = false;
156 searchitems = '';
157 } else { // build our html
158 // console.log(results)
159 permalinks = [];
160 numLimit = 5;
161 for (let item in results) { // only show first 5 results
162 if (item > numLimit) {
163 break;
164 }
165 if (permalinks.includes(results[item].item.permalink)) {
166 continue;
167 }
168 // console.log('item: %d, title: %s', item, results[item].item.title)
169 searchitems = searchitems + '<li><a href="' + results[item].item.permalink + '" tabindex="0">' + '<span class="title">' + results[item].item.title + '</span></a></li>';
170 permalinks.push(results[item].item.permalink);
171 }
172 resultsAvailable = true;
173 }
174
175 document.getElementById("searchResults").innerHTML = searchitems;
176 if (results.length > 0) {
177 first = list.firstChild.firstElementChild; // first result container — used for checking against keyboard up/down location
178 last = list.lastChild.firstElementChild; // last result container — used for checking against keyboard up/down location
179 }
180}
添加HTML代码到主题里
这里因为每个主题可能存在差异,所以请根据自己实际的情况做出相应的更改。我选择将代码添加到页头的菜单栏后面,在/layouts/partials/header.html添加
1 <li class="menu-item">
2 <a id="search-btn" style="display: inline-block;" href="javascript:void(0);">
3 <i class="iconfont">
4 {{ partial "svg/search.svg" }}
5 </i>
6 </a>
7 <div id="fastSearch">
8 <input id="searchInput" tabindex="0">
9 <ul id="searchResults">
10 </ul>
11 </div>
12 </li>
li标签是继承主题,i标签是因为调用了图标。
在主题模板上引用js
我使用的主题有一个专门引用js的模板,所以我选择在此添加引用。选择/layouts/partials/scripts.html添加
1<!-- Fastsearch -->
2<script src="/js/fuse.min.js"></script>
3<script src="/js/fastsearch.js"></script>
添加CSS样式
添加样式我们尽量选择对应的模板来添加,比如说我是在header里修改的,那么我就直接选择在/assets/sass/_partial/_header.scss添加CSS样式了。如果你使用的主题没有模板CSS的话,直接在主题的主CSS上添加。
1#fastSearch {
2 visibility: hidden;
3 position: absolute;
4 right: 0px;
5 top: 30px;
6 display: inline-block;
7 width: 320px;
8 margin: 0 10px 0 0;
9 padding: 0;
10}
11
12#fastSearch input {
13 padding: 4px;
14 width: 100%;
15 height: 31px;
16 font-size: 1em;
17 color: #465373;
18 font-weight: bold;
19 background-color: #95B0F4;
20 border-radius: 3px 3px 0px 0px;
21 border: none;
22 outline: none;
23 text-align: left;
24 display: inline-block;
25}
26
27#fastSearch ul {
28 list-style: none;
29 margin: 0px;
30 padding: 0px;
31}
32
33#searchResults li {
34 list-style: none;
35 margin-left: 0em;
36 background-color: #E1E7F7;
37 border-bottom: 1px dotted #465373;
38}
39
40#searchResults li .title {
41 font-size: .9em;
42 margin: 0;
43 display: inline-block;
44}
45
46#searchResults {
47 visibility: inherit;
48 display: inline-block;
49 width: 328px;
50 margin: 0;
51 max-height: calc(100vh - 120px);
52 overflow: hidden;
53}
54
55#searchResults a {
56 text-decoration: none !important;
57 padding: 10px;
58 display: inline-block;
59 width: 100%;
60}
61
62#searchResults a:hover, #searchResults a:focus {
63 outline: 0;
64 background-color: #95B0F4;
65 color: #fff;
66}
67
68#search-btn {
69 position: sticky;
70 font-size: 20px;
71}
这里需要根据自己主题来进行稍微的修改。
总结
其实跟着官方的教程一步步来集成还是很简单的,十分、八分钟吧。在使用后觉得反应是真的快,比之前使用的Algolia快太多了,而且还方便,Algolia在hugo
之后还得输入npm run algolia
,fastsearch不需任何命令,正常的hugo
就行。