menu search

Hugo 本地搜索

Jul 22, 2017

静态博客的搜索功能实现基本上可以分为两种:第一种是生成所有数据,客户端下载这些数据并进行搜索;第二种是借助第三方服务来进行索引,并返回匹配的数据。

前一种实现起来很简单,毕竟不需要研究第三方API。但如果需要搜索的数据非常大,或者是客户端的性能不够就容易导致卡顿。

这篇文章就写写第一种在Hugo上的实现方法好了。

想法

  1. Hugo 生成全站文章的数据,以JSON格式保存
  2. 浏览器下载这些数据
  3. 前端找个JS插件来索引数据 (Lunr, Fuse…)
  4. 返回匹配数据,并渲染列表

演示

搜索

实现

/// config.toml
[outputs]
  home = ["HTML", "RSS", "JSON"]
/// YOUR_TEMPLATE/partials/index.json
[
    {{ range $index, $element := (where .Site.RegularPages "Type" "post") }}
        {
            "title" : {{ jsonify .Title }},
            "date" : {{- jsonify .Date.Format ( or .Site.Params.dateFormat "2006, Jan 02" ) -}},
            "url" : {{ jsonify .Permalink }},
            "content": {{ jsonify .Summary }},
            "tag" : {{ jsonify .Params.Tag }}
        }{{if not (eq $index (sub (len (where .Site.RegularPages "Type" "post")) 1 )) }} , {{end}}
    {{ end }}
]

/// YOUR_TEMPLATE/partials/_default/search.html

<main id="searchLayout" class="container">
    <input id="search-input" type="text" placeholder="Type something and hit enter"/>
    <p id="search-info"></p>
    <div id="search-result"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fuse.js/3.0.5/fuse.min.js"></script>
    <script src="search.js"></script>
</main>
var search = function () {

    var searchOptions = {
        shouldSort: true,
        tokenize: true,
        threshold: 0.1,
        location: 0,
        distance: 100,
        maxPatternLength: 32,
        minMatchCharLength: 1,
        keys: [
            "title",
            "content",
            "tag"
        ]
    };

    var searchResult = getElement("search-result", 'id'),
        searchInput = getElement("search-input", 'id'),
        searchInfo = getElement('search-info', 'id');

    var posts = [],
        fuse;

    if (!posts.length) {
        fetch('/index.json').then((res) => res.json()).then(response => { //获取内容
            posts = response;
            bindSearch();
        })
    } else {
        bindSearch();
    }

    function bindSearch() {
        fuse = new Fuse(posts, searchOptions); //初始化Fuse

        if (searchInput.value !== '') { // 以防用户在数据加载之前已经输入
            doSearch(searchInput.value);
        };

        searchInput.addEventListener('change', function (e) {
            var value = e.target.value;

            if (value == '') {
                return;
            };

            doSearch(value);
        });
    };

    function doSearch(keyword) {
        searchInfo.innerHTML = ''; //清空内容
        searchResult.innerHTML = '';

        var results = fuse.search(keyword);

        if (!results.length) {
            searchInfo.innerHTML = `Have not found any post related to keyword <strong>${keyword}</strong>`;
            return;
        } else {
            searchInfo.innerHTML = `Found ${results.length} posts related to keyword <strong>${keyword}</strong>`;
        }

        results.forEach(function (result, i) {
            var item = document.createElement('div');
            var html = `<article>
                        <header class="post-header">
                            <h2 class='post-title'>
                                <a href='${ result.url }'>
                                    ${ result.title }
                                </a>
                            </h2>
                        </header>
                        <section class="post-excerpt">
                            <p>${ result.content }</p>
                        </section>
                        <section class="post-meta">`;

            if (result.tag.length) {
                html += `<a href='/tag/${ encodeURIComponent(result.tag[0].toLocaleLowerCase()) }'>${result.tag[0]}</a>`;
            };

            html += `
                        <time class="post-date">
                                ${ result.date }
                            </time>
                        </section>
                    </article>
                `;
            item.innerHTML = html;

            searchResult.appendChild(item);
        });
    }
}

Gist

配置

我使用了Fuse这个库来做索引。当然也你也可以换其他的JS库,我只是随便找了个能用的…

index.json这个文件需要放到你的模板目录下的partials下,同时修改博客根目录的config.toml,加入下面这段:

[outputs]
  home = ["HTML", "RSS", "JSON"]

生成的JSON会存放在博客的根目录,可以通过domain.com/index.json来访问。

index.json里你会发现,目前输出的内容有:

  • title : 标题
  • date : 日期
  • url: 地址
  • tag: 标签。请注意,我使用的是.Params.tag,而不是.Params.tags
  • content: 概要,并不是全文

目前只会搜索文章的概要、标签和标题,如果需要搜索更多内容,可以修改search.js的第11行。虽然我并不推荐搜索全文,因为数据会太大,导致速度变慢。

脚本里面用到了fetch和ES6的Template Strings。后者不被IE支持,所以如果需要兼容的话还是慢慢拼字符串吧。(search.js 67 - 90 行)😂

如果让我选,我选择放弃IE,这样写模板多舒服…

另外,在search.js开头有个searchOptions,那是Fuse的设置,可以到官网查询每个选项的意思。

我认为最重要的还是threshold。修改这个值 ([0, 1]) 可以更改搜索的准确度,越小越准确。


最后,你需要创建一个页面,并在Frontmatter注明layout: 'search',例如:

---
title: "搜索"
date: '2017-07-22'
slug: "search"
layout: 'search'
---

最后

如果你的博文很多,不推荐使用此方法进行搜索:容易拖慢生成速度不说,客户端也要下载大量数据&处理。

我现在文章数量还不多,先暂时用着,但很有可能我以后会换Google Custom Search,用起来更舒服。

Photo by Nine Köpfer on Unsplash

Comments

edit x send markdown image
paragraph comment heart