返回

给 Hugo 博客增加搜索功能

   
摘要GPT
摘要小助理暂时失联跑路啦……😜

此方法来自 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,不过我使用这个生成索引失败了,没有任何错误输出,只能做罢。

亮点

  1. 最小/零外部依赖(无需 jQuery)
  2. 添加到每个页面尺寸尽可能小
  3. JSON 索引文件按需加载(进一步减少对页的速度/用户体验的整体影响)
  4. 键盘友好,瞬时导航(有点像 Alfred / macOS Spotlight)

另外,此方案就像 Eddie Webb指出的那样, 还有如下额外的好处:

  1. 无需 NPM, grunt 等外部工具
  2. 无需额外的编译步骤,你只需要像往常一样执行 hugo
  3. 可以方便地切换到任意可使用 json 索引的客户端搜索工具

集成步骤

  1. 添加 index.json 文件到 layouts/_default
  2. 修改 config.toml 以使 Hugo 对首页生成额外的 JSON 输出格式
  3. 添加 fastsearch.js 和 fuse.min.js (可从 https://fusejs.io 下载) 到 static/js
  4. 添加搜索框 HTML 代码到模板页面 footer
  5. 添加 CSS 样式到模板页面 header 或模板主 CSS 文件
  6. 访问 http://localhost:1313/ , 键入 Alt-/ 执行搜索

相关文件

注意:跟原文章相比,老灯做了一些微调

  1. 允许通过点击页面空白处隐藏搜索框,而不是只能按 Esc

  2. 在右上角添加了一个搜索按钮,方便不想按快捷键的人

  3. 默认的快捷键由于 Firefox Linux 默认 Super-/ 是 Quick Find 功能,因此我改成了 Alt-/

  4. 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, 如果文章数量特别多,可能会导致生成的索引过大

  1. config.toml 增加配置
[outputs]
  home = ["HTML", "RSS", "JSON"]
  1. 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
  }
}
  1. 添加搜索框 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>
  1. 添加 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;
    }
  }

如果样式跟你当前的主题不是很合,你可以自行稍作调整。

知识共享许可证 CC BY-NC-SA 4.0
最后更新于 2024-07-17 18:28
使用 Hugo 构建
主题 hugo-magic小洋葱 魔改 由 Jimmy 设计
Written by Human, Not by AI