此方法来自 Hugo官方文档 中的 hugofastsearch
A usability and speed update to “Github Gist for Fuse. Js integration” — global, keyboard-optimized search.
没错,这个方案,是 Github Gist for Fuse.js integration 的改进版。
其实在使用这个方案之前,老灯也尝试了 hugo-lunr-zh
方案。hugo-lunr Last publish 4 years ago 而 hugo-lunr-zh 本身是基于 hugo-lunr
添加了一个 nodejieba (结巴分词 lib)分词的功能以支持中文,同样是年久失修了 Last publish 2 years ago,不过我使用这个生成索引失败了,没有任何错误输出,只能做罢。
亮点
- 最小/零外部依赖(无需 jQuery)
- 添加到每个页面尺寸尽可能小
- JSON 索引文件按需加载(进一步减少对页的速度/用户体验的整体影响)
- 键盘友好,瞬时导航(有点像 Alfred / macOS Spotlight)
另外,此方案就像 Eddie Webb指出的那样, 还有如下额外的好处:
- 无需 NPM, grunt 等外部工具
- 无需额外的编译步骤,你只需要像往常一样执行
hugo
- 可以方便地切换到任意可使用 json 索引的客户端搜索工具
集成步骤
- 添加
index.json
文件到layouts/_default
- 修改
config.toml
以使 Hugo 对首页生成额外的JSON
输出格式 - 添加
fastsearch.js
和fuse.min.js
(可从 https://fusejs.io 下载) 到static/js
- 添加搜索框 HTML 代码到模板页面 footer
- 添加 CSS 样式到模板页面 header 或模板主 CSS 文件
- 访问 http://localhost:1313/ , 键入
Alt-/
执行搜索
相关文件
注意:跟原文章相比,老灯做了一些微调
允许通过点击页面空白处隐藏搜索框,而不是只能按 Esc
在右上角添加了一个搜索按钮,方便不想按快捷键的人
默认的快捷键由于 Firefox Linux 默认
Super-/
是 Quick Find 功能,因此我改成了Alt-/
layouts/_default/index.json
{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
{{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "categories" .Params.categories "contents" .Plain "permalink" .Permalink "date" .Date "section" .Section) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
这里默认取的
contents
, 如果文章数量特别多,可能会导致生成的索引过大
config.toml
增加配置
[outputs]
home = ["HTML", "RSS", "JSON"]
static/js/fastsearch.js
fuse.min.js
可从 https://github.com/krisk/Fuse/releases 下载。
var fuse; // holds our search engine
var fuseIndex;
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('keydown', function(event) {
// CMD-/ to show / hide Search
if (event.altKey && event.which === 191) {
// 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
doSearch(event)
}
// Allow ESC (27) to close search box
if (event.keyCode == 27) {
if (searchVisible) {
document.getElementById("fastSearch").style.visibility = "hidden";
document.activeElement.blur();
searchVisible = false;
}
}
// DOWN (40) arrow
if (event.keyCode == 40) {
if (searchVisible && resultsAvailable) {
console.log("down");
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);
}
Document.QuerySelector ("body"). Onclick = function (e) {
if (e.target. TagName === 'BODY' || e.target. TagName === 'DIV') {
HideSearch ()
}
}
document.QuerySelector (" #search -btn"). Onclick = function (e) {
DoSearch (e)
}
Function doSearch (e) {
e.stopPropagation ();
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) {
ShowSearch () // search visible
}
Else {
HideSearch ()
}
}
Function hideSearch () {
Document.GetElementById ("fastSearch"). Style. Visibility = "hidden" // hide search box
Document.ActiveElement.Blur () // remove focus from search box
SearchVisible = false
}
Function showSearch () {
Document.GetElementById ("fastSearch"). Style. Visibility = "visible" // show search box
Document.GetElementById ("searchInput"). Focus () // put focus in input box so you can just start typing
SearchVisible = true
}
// ==========================================
// 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 () {
Console.Log ('loadSearch ()')
FetchJSONFile ('/index. Json', function (data){
Var options = { // fuse. Js options; check fuse. Js website for details
ShouldSort: true,
Location: 0,
Distance: 100,
Threshold: 0.4,
MinMatchCharLength: 2,
Keys: [
'permalink',
'title',
'tags',
'contents'
]
};
// Create the Fuse index
FuseIndex = Fuse.CreateIndex (options. Keys, data)
Fuse = new Fuse (data, options, fuseIndex); // 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) {
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 = '';
} else { // build our html
// console.Log (results)
Permalinks = [];
NumLimit = 5;
For (let item in results) { // only show first 5 results
If (item > numLimit) {
Break;
}
If (permalinks.Includes (results[item]. Item. Permalink)) {
Continue;
}
// console.Log ('item: %d, title: %s', item, results[item]. Item. Title)
searchitems = searchitems + '<li><a href="' + results[item].item.permalink + '" tabindex="0">' + '<span class="title">' + results[item]. Item. Title + '</span></a></li>';
Permalinks.Push (results[item]. Item. Permalink);
}
ResultsAvailable = true;
}
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
}
}
- 添加搜索框 HTML 代码到模板页面 footer
这个可以通过添加到 baseof 或者 footer 模板。
比如我当前在使用的 terminal 主题,它就内置了额外的 footer 支持,可以通过添加 layouts/partials/extended_footer. Html
方便地对 footer 增加内容。
如果主题没有额外的支持,你可以 copy 你当前主题目录下的 baseof. Html 模板到layouts/_default/baseof. Html
,然后在最后附加内容。
<a id="search-btn" style="display: inline-block;" href="javascript:void(0);">
<span class="icon-search">捜</span>
</a>
<div id="fastSearch">
<input id="searchInput" tabindex="0">
<ul id="searchResults">
</ul>
</div>
<script src="/js/fuse.min.js"></script> <!-- download and copy over fuse.min.js file from fusejs.io -->
<script src="/js/fastsearch.js"></script>
- 添加 CSS 样式到模板页面 header 或模板主 CSS 文件
这个可以通过添加到 header 模板或模板的主 CSS 文件。
比如我当前在使用的 terminal 主题,它就内置了额外的 header 支持,可以通过添加 layouts/partials/extended_header. Html
方便地对 header 增加内容。
如果主题没有额外的支持,你可以修改模板的主 CSS 文件,通常是style. Css
或 main. Css
,这个因情况而异。
#fastSearch {
Visibility: hidden;
Position: absolute;
Right: 10 px;
Top: 10 px;
Display: inline-block;
Width: 320 px;
Margin: 0 10 px 0 0;
Padding: 0;
}
#fastSearch input {
Padding: 4 px;
Width: 100%;
Height: 31 px;
Font-size: 1.6 em;
color: #222129 ;
Font-weight: bold;
background-color: #ffa86a ;
Border-radius: 3 px 3 px 0 px 0 px;
Border: none;
Outline: none;
Text-align: left;
Display: inline-block;
}
#searchResults li {
List-style: none;
Margin-left: 0 em;
background-color: #333 ;
border-bottom: 1 px dotted #000 ;
}
#searchResults li .title {
Font-size: 1.1 em;
Margin: 0;
Display: inline-block;
}
#searchResults {
Visibility: inherit;
Display: inline-block;
Width: 320 px;
Margin: 0;
Max-height: calc (100 vh - 120 px);
Overflow: hidden;
}
#searchResults a {
Text-decoration: none !Important;
Padding: 10 px;
Display: inline-block;
Width: 100%;
}
#searchResults a: hover, #searchResults a: focus {
Outline: 0;
background-color: #666 ;
color: #fff ;
}
#search -btn {
Position: absolute;
Top: 10 px;
Right: 20 px;
Font-size: 24 px;
}
@media (max-width: 683 px) {
#fastSearch , #search -btn {
Top: 64 px;
}
}
如果样式跟你当前的主题不是很合,你可以自行稍作调整。