4 min read
Simple Hugo page search with Lunr.js
Hugo is static website engine and acutally used to generated this blog post. Due to its static nature, there is now way to provide serverside capablities for dynamic lookups such as used by a search feature. Everything dynamically has to be done client side. In this post I am going to setup a search page for a Hugo site the most simple way.
Hugo can not only generate HTML documents, but also JSON documents that can be indexed by Lunr.js. Lunr.js is a small, full-text search library for use in the browser. Using a JSON document and the libararz we are going to create a page for site wide full-text search.
In our scenario we assume that the following Hugo content structure exists:
.
├── content
│ ├── page
│ │ ├── about.md
│ │ ├── archive.md
│ │ └── search.md
│ └── post
│ ├── lithium.md
│ └── markdown.md
There are two sections page and post.
We would like to generate a JSON output for the post section. To do so the supported output format has be enabled in the configuration file.
config.toml
[outputs]
section = ["JSON", "HTML"]
In addition the current theme has to have a .json
list template. Either this template is defined as default template or for the post section only, which I prefer.
themes/THEME_NAME/layouts/post/list.json
[
{{ range $index, $value := where .Site.Pages "Type" "post" }}
{{ if $index }}, {{ end }}
{
"url": "{{ .RelPermalink }}",
"title": "{{ .Title }}",
"content": {{ .Content | plainify | jsonify }}
}
{{ end }}
]
This template creates JSON object for each blog post. The url acts as an identifier and the fields title and content will be indexed for the full text search.
If the output configuration has been applied, the template created and Hugo server started, a JSON document should be available at http://localhost:1313/post/index.json. This document will be indexed by Lunr.js.
In our next and final step, we are going to create the acutal search page. In order to understand the logic of the page I have dismantled it into 5 blocks.
content/page/search.md
<script src="https://unpkg.com/lunr/lunr.js"></script>
<script type="text/javascript">
<!--...-->
The Lunr.js library is included at the beginning of the page.
//...
// define globale variables
var idx, searchInput, searchResults = null
var documents = []
function renderSearchResults(results){
if (results.length > 0) {
// show max 10 results
if (results.length > 9){
results = results.slice(0,10)
}
// reset search results
searchResults.innerHTML = ''
// append results
results.forEach(result => {
// create result item
var article = document.createElement('article')
article.innerHTML = `
<a href="${result.ref}"><h3 class="title">${documents[result.ref].title}</h3></a>
<p><a href="${result.ref}">${result.ref}</a></p>
<br>
`
searchResults.appendChild(article)
})
// if results are empty
} else {
searchResults.innerHTML = '<p>No results found.</p>'
}
}
//...
The renderSearchResults
processes the search results generated by Lunr.js and appends them on a specific DOM element.
//...
function registerSearchHandler() {
// register on input event
searchInput.oninput = function(event) {
// remove search results if the user empties the search input field
if (searchInput.value == '') {
searchResults.innerHTML = ''
} else {
// get input value
var query = event.target.value
// run fuzzy search
var results = idx.search(query + '*')
// render results
renderSearchResults(results)
}
}
// set focus on search input and remove loading placeholder
searchInput.focus()
searchInput.placeholder = ''
}
//...
The registerSearchHandler
function attaches the search call on the input event of the search input field.
//...
window.onload = function() {
// get dom elements
searchInput = document.getElementById('search-input')
searchResults = document.getElementById('search-results')
// request and index documents
fetch('/post/index.json', {
method: 'get'
}).then(
res => res.json()
).then(
res => {
// index document
idx = lunr(function() {
this.ref('url')
this.field('title')
this.field('content')
res.forEach(function(doc) {
this.add(doc)
documents[doc.url] = {
'title': doc.title,
'content': doc.content,
}
}, this)
})
// data is loaded, next register handler
registerSearchHandler()
}
).catch(
err => {
searchResults.innerHTML = `<p>${err}</p>`
}
)
}
//...
Once the browser has loaded the DOM structure, the post list is retrieved and indexed.
<!--...-->
</script>
<input id="search-input" type="text" placeholder="Loading..." name="search">
<section id="search-results" class="search"></section>
Only the search input field is actually visible.
This page can be mapped to /search
. For a full view of the search.md
file, check it out in the example site of the Hugo lithium theme.
Hope this article helped adding a search feature to your Hugo site 😊
And as always remember:
Satisfying solutions call for simple actions.
Sources
Of course I had some help creating this search feature for Hugo. Props to these posts:
Foresty - Build a JSON API With Hugo’s Custom Output Formats
Matt Walters - Hugo and Lunr
Tags: search , lunrjs , static , javascript
Edit this page
Show statistic for this page