Static site search using jQuery and JSON

Static site search using jQuery and JSON

Add search function to your static site using jQuery and JSON

An example on how to add search function to your (Hugo) static site using jQuery and JSON.

Static sites have many advantages including fast-loading, lower server requirement, a lot of flexibility and many free hosting services. Many static site generators have appeared in the recent years and some of them got quite popular, e.g., Jekyll, Hugo, Hexo and Middleman. But one of the downsides is the search function.

One of the general solutions is to use a third-party searching engine like Google site search, but you might not get what you were looking for because it takes a while unitl your site content has been indexed. So many people wish to have a client-side search function on their sites. For sites built with Hexo the Algolia plugin has been well supported, but currently not for Jekyll and Hugo sites.

But luckily there is a good searching solution using jQuery + JSON, and another JavaScript library like Lunr.js, and Fuse.js. Here is the main steps:

Build the JSON index database

Hogo site solution

See the discussion and inspiration here.

  • Create a dummy content file like content/json.md with the following content:
+++
type = "json"
+++
  • Setup a template for this data type: to create layouts/json/single.html with content like this (modify to your own needs):
[{{ range $index, $page := .Site.Pages }}{{ if ne $page.Type "json" }}{{ if $index }},{{ end }}
{
    "href": "{{ $page.RelPermalink }}",
    "title": "{{ $page.Title }}",
    "tags": [{{ range $tindex, $tag := $page.Params.tags }}{{ if $tindex }}, {{ end }}"{{ $tag }}"{{ end }}],
    "content": "{{ $page.PlainWords }}"
}{{ end }}{{ end }}
]
  • Run Hugo build, change /Public/json/index.html to PagesIndex.json, delete the first “,” and move it to your index directory (e.g., /js/lunr/PagesIndex.json)

  • Resulting JSON looks like this (THE LAST LINE OF EACH ITEM HAS NO ‘,’ AT THE END)

[
{
    "title": "Smp_000020",
    "href": "/geneexp/sm/Smp_000020.1/",
    "content": "bifunctional protein NCOAT"
},
{
    "title": "Smp_000030",
    "href": "/geneexp/sm/Smp_000030.1/",
    "content": "26s proteasome regulatory particle subunit"
},
//...
]

Middleman site solution

See example here using ERB.

Add the JS libraries and script functions to your html page

In the script you can define the content to be searched and the conten to be shown.

Example using Lunr.js

    <script type="text/javascript" src="/js/jquery.min.js"></script>
    <script type="text/javascript" src="/js/vendor/lunr.min.js"></script>
    <script type="text/javascript">
    var lunrIndex,
        $results,
        pagesIndex;

    // Initialize lunrjs using our generated index file
    function initLunr() {
        // First retrieve the index file
        $.getJSON("/js/lunr/PagesIndex.json")
            .done(function(index) {
                pagesIndex = index;
                console.log("index:", pagesIndex);

                // Set up lunrjs by declaring the fields we use
                // Also provide their boost level for the ranking
                lunrIndex = lunr(function() {
                    this.field("title", {
                        boost: 10
                    });
                    // this.field("tags", {
                    //    boost: 5
                    //});
                    // this.field("content");

                    // ref is the result item identifier (I chose the page URL)
                    this.ref("href");
                });

                // Feed lunr with each file and let lunr actually index them
                pagesIndex.forEach(function(page) {
                    lunrIndex.add(page);
                });
            })
            .fail(function(jqxhr, textStatus, error) {
                var err = textStatus + ", " + error;
                console.error("Error getting Hugo index flie:", err);
            });
    }

    // Nothing crazy here, just hook up a listener on the input field
    function initUI() {
        $results = $("#results");
        $("#search").keyup(function() {
            $results.empty();

            // Only trigger a search when 2 chars. at least have been provided
            var query = $(this).val();
            if (query.length < 8) {
                return;
            }

            var results = search(query);

            renderResults(results);
        });
    }

    /**
     * Trigger a search in lunr and transform the result
     *
     * @param  {String} query
     * @return {Array}  results
     */
    function search(query) {
        // Find the item in our index corresponding to the lunr one to have more info
        // Lunr result: 
        //  {ref: "/section/page1", score: 0.2725657778206127}
        // Our result:
        //  {title:"Page1", href:"/section/page1", ...}
        return lunrIndex.search(query).map(function(result) {
                return pagesIndex.filter(function(page) {
                    return page.href === result.ref;
                })[0];
            });
    }

    /**
     * Display the 10 first results
     *
     * @param  {Array} results to display
     */
    function renderResults(results) {
        if (!results.length) {
            return;
        }

        // Only show the ten first results
        results.slice(0, 10).forEach(function(result) {
            var $result = $("<li>");
            $result.append($("<a>", {
                href: result.href,
                text: "» " + result.title + " "
            }), result.content);
            $results.append($result);
        });
    }

    // Let's get started
    initLunr();

    $(document).ready(function() {
        initUI();
    });
    </script>

Example using Fuse.js

Script take from here.

// Extract the `q` query parameter
var queryStringRegex = /[\?&]q=([^&]+)/g;
var matches = queryStringRegex.exec(window.location.search);
if(matches && matches[1]) {
  var value = decodeURIComponent(matches[1].replace(/\+/g, '%20'));

  // Load the posts to search
  $.getJSON('/search/posts.json').then(function(posts) {
    // Remember to include Fuse.js before this script.
    var fuse = new Fuse(posts, {
      keys: ['title', 'tags'] // What we're searching
    });

    // Run the search
    var results = fuse.search(value);

    // Generate markup for the posts, implement SearchResults however you want.
    var $results = SearchResults(results);

    // Add the element to the empty <div> from before.
    $('#searchResults').append($results);
  });
}
  <div id="search-bar" class="search-bar">
    <input id="search" type="search" placeholder="Search..."><br>
  </div>

  <div id="results" style="margin-left: 28%;"></div>

Then you should be able to see the live search results like this:

  • Lunr.js example
  • Fuse.js example (hit Enter and show results)

Alright, you got a search for your static site!

PS. There are also some other interesting JS libraries to do the job:

  • List.js with sort and filter functions
  • Select2 with select boxes
  • js-search implementing Lunr.js with flexibilities.

UPDATE 2017.05.22 just switched to another brilliant JS library Tipue Search

TipueSearch has more flexibilities in searching mode, search sets, related search and other output options.

Below is the template to generate the “tipuesearch_content.js” file (by changing name of resulting tipuesearch/index.html)

var tipuesearch = {"pages": [{{ range $index, $page := .Site.Pages }}{{ if eq $page.Type "post" }}{{ if $index }},{{ end }}
     {"title": "{{ $page.Title }}",  "text": "{{ $page.Params.description }}", "tags": "{{ range $tindex, $tag := $page.Params.tags }}{{ if $tindex }} {{ end }}{{ $tag }}{{ end }}", "url": "{{ .Site.BaseURL }}{{ $page.RelPermalink }}"}{{ end }}{{ end }}
]};
Z. Lu avatar
Z. Lu
Data science, bioinfo, scripting, parasites, retro, plain text.
comments powered by Disqus