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 the previous 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 added layouts/search-index/single.html
and 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 a exclude_search
parameter 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 the JSON
file 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 an index.json
file in the root of your site, that will be filled with a JSON formatted document of your posts/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 the search.js
file 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 your index.json
file by running Hugo. I found that if I also had the Hugo server running while I build index.json
, then I had to stop the Hugo server and restart it.