我的部落格使用了 hugo 作為靜態生成工具,自帶的主題裡也沒有附帶搜尋功能。看來,還是得自己給部落格新增一個搜尋功能。
經過多方查詢,從 Hugo Fast Search · GitHub 找到一片詳細、可用的教學(雖然後面魔改了一些)。
layouts/_default
目錄下新增 index.json 檔案;[params]
# 是否開啟本地搜尋
fastSearch = true
[outputs]
# 增加 JSON 設定
home = ["HTML", "RSS", "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/[email protected]"></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 頁面的程式碼分為兩個部分:搜尋的按鈕、搜尋方塊和結果展示。
我這裡將搜尋的按鈕放到的選單欄,主要是一個可點選的按鈕:
{{ 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 }}
頁面樣式這部分,主要是看個人的喜好,這裡只放出自己的樣式:
#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;
}
}
}
經過兩天時間的奮鬥,終於是將搜尋功能給上線了。
不得不說,理想總是一開始美好,最初以為是一個完整、可用的教學,卻沒想到複製到程式碼之後就不可用了,最終是經過自己的魔改才得以使用。
總結一下就是,沒有實踐就沒有話語權,千萬不要做管中窺豹的那個人。