This page looks best with JavaScript enabled

Hugo and Lunrjs search engine made simple

 · ☕ 5 min read · 👽 john hashim

Getting Hugo and Lunr playing nice with each other wasn’t too bad, but I had trouble finding a clear walk-through with current versions of the tools. My starting point was with theprevious post, Search your Hugo static site using lunr.js. That was the most complete guide I could find and got me … 80% of the way there. I think the hiccups I encountered were due to updates to the Lunr code and some breaking changes that happened since he wrote his post.

The first step of his guide worked well for me, though I made some small edits to suit my setup. In my theme folder I addedlayouts/search-index/single.htmland provided the following contents:

1
2
3
4
5
{{- $.Scratch.Add "index" slice -}}
{{- range where .Site.Pages ".Params.exclude_search" "!=" true -}}
{{- $.Scratch.Add "index" (dict "title" .Title "ref" .Permalink "body" .Plain "excerpt" .Summary) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

The main change I made here was in the range where statement. I decided to add aexclude_searchparameter to my Front Matter. Setting this to true, will exclude that page from search results.

A second change I made was to include some extra fields in theJSONfile that that template will create1. It adds the body of the page, with HTML stripped out, so the content can be fully searched. It also adds the Summary for the content that can be used when displaying search results.

After setting that up, building your site by running Hugo will produce anindex.jsonfile in the root of your site, that will be filled with a JSON formatted document of yourposts/pages/etc.

From here, we can move onto the actual Lunr implementation.

I’ll try to boil this code down to just the most pertinent information2 that should apply for most implementations. I have some customizations in mine that are particular to my site that aren’t included here. On my search page, I include the Lunr library from a CDN and my sites search code. Here is what my entire search.md file looks like so you can see the search input field as well.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
---
title: "Search"
date: 2018-03-27T16:16:11-04:00
layout: page
exclude_search: true
---
<script src="https://unpkg.com/lunr/lunr.js"></script>
<script src="/search.js"></script>

<div>
  <input id="search-input" type="text" 
placeholder="What are you looking for?" 
name="search-input" class="form-control">
</div>
<div id="search-results" class="container"></div>

Here’s some explanation of thesearch.jsfile which is placed in the /static/ directory.

We start by setting up some global variables:

1
2
3
4
var idx = null;         // Lunr index
var resultDetails = []; // Will hold the data for the search results (titles and summaries)
var $searchResults;     // The element on the page holding search results
var $searchInput;       // The search box element

Now we set up the onLoad event:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
window.onload = function () {
  // Set up for an Ajax call to request the JSON data file that is created by
  // Hugo's build process, with the template we added above
  var request = new XMLHttpRequest();
  var query = '';

  // Get dom objects for the elements we'll be interacting with
  $searchResults = document.getElementById('search-results');
  $searchInput   = document.getElementById('search-input');

  request.overrideMimeType("application/json");
  request.open("GET", "/index.json", true); // Request the JSON file created during build
  request.onload = function() {
    if (request.status >= 200 && request.status < 400) {
      // Success response received in requesting the index.json file
      var documents = JSON.parse(request.responseText);

      // Build the index so Lunr can search it.  The `ref` field will hold the URL
      // to the page/post.  title, excerpt, and body will be fields searched.
      idx = lunr(function () {
        this.ref('ref');
        this.field('title');
        this.field('excerpt');
        this.field('body');

        // Loop through all the items in the JSON file and add them to the index
        // so they can be searched.
        documents.forEach(function(doc) {
            this.add(doc);
            resultDetails[doc.ref] = {
              'title': doc.title,
              'excerpt': doc.excerpt,
            };
        }, this);
      });
    } else {
      $searchResults.innerHTML = 'Error loading search results';
    }
  };

  request.onerror = function() {
    $searchResults.innerHTML = 'Error loading search results';
  };

  // Send the request to load the JSON
  request.send();

  // Register handler for the search input field
  registerSearchHandler();
};

We need to perform searches when someone types something in the search box, so here is the handler. Whenever there is typing in the search input field, a search will be performed by Lunr:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function registerSearchHandler() {
  $searchInput.oninput = function(event) {
    var query = event.target.value;
    var results = search(query);  // Perform the search

    // Render search results
    renderSearchResults(results);

    // Remove search results if the user empties the search phrase input field
    if ($searchInput.value == '') {
      $searchResults.innerHTML = '';
    }
  }
}
Once a user enters a search term, there will be a call to `renderSearchResults()`  to show the results on the page:

function renderSearchResults(results) {
  // Create a list of results
  var ul = document.createElement('ul');
  if (results.length > 0) {
    results.forEach(function(result) {
      // Create result item
      var li = document.createElement('li');
      li.innerHTML = '<a href="' + result.ref + '">' + resultDetails[result.ref].title + '</a><br>' + resultDetails[result.ref].excerpt;
      ul.appendChild(li);
    });

    // Remove any existing content so results aren't continually added as the user types
    while ($searchResults.hasChildNodes()) {
      $searchResults.removeChild(
        $searchResults.lastChild
      );
    }
  } else {
    $searchResults.innerHTML = 'No results found';
  }

  // Render the list
  $searchResults.appendChild(ul);
}

The last missing piece is telling Lunr to perform the search:

1
2
3
function search(query) {
  return idx.search(query);
}

After that, you should be able to perform searches on your site by visiting the search page on your site.

It’s worth noting that if you are trying this locally using Hugo’s local server functionality, you need to remember to build yourindex.jsonfile by running Hugo. I found that if I also had the Hugo server running while I buildindex.json, then I had to stop the Hugo server and restart it.

Share on

john hashim
WRITTEN BY
john hashim
Web Developer