I’ve been trying to find a really simple search solution for Hugo for a long time, and I think I’ve finally cobbled something together that is genuinely drop-in and go.
It’s not super pretty, but that means you can style it to suit your own needs.
Like most of the other solutions I’ve seen out there, this requires a JSON index to be generated at build time, so the first thing to do is update your config.toml file so that Hugo outputs JSON:
[outputs]
home = ["HTML", "RSS", "JSON"]
Next you’ll need a template file for building the JSON, nothing unusual here, just a file called index.json in your /layouts/_default directory. Its contents should look like this (the only thing you might not have is what I’ve called ‘abstract’, which gets written to the ‘summary’ key, but you could easily adjust this so that it picks up the first couple hundred words in your article, or whatever you like):
{{/* layouts/_default/index.json */}}
{{- $index := slice -}}
{{- range where .Site.RegularPages.ByDate.Reverse "Type" "not in" (slice "page" "json") -}}
{{- $index = $index | append (dict "title" ( .Title | plainify ) "permalink" .Permalink "tags" ( .Params.tags ) "categories" ( .Params.categories ) "summary" ( .Params.abstract | markdownify | htmlUnescape | plainify) ) -}}
{{- end -}}
{{- $index | jsonify -}}
So far, so good, you have a JSON file!
Last bit is to drop-in the code for searching - if you want you can split this out into dedicated Javascript and CSS files, but the beauty is that you don’t need to if you don’t want to - this will work just as it is.
<style>
.elbi-results {
width: 100%;
}
.elbi-results-item {
background-color: #FFFFFF;
color: #000000;
padding: 10px;
border: 1px solid black;
}
</style>
<div>
<input type="text" id="elbi-input" placeholder="Search the site...">
<ul id="elbi-results"></ul>
</div>
<script>
const input = document.getElementById("elbi-input");
const results = document.getElementById("elbi-results");
const request = new Request("/index.json");
fetch(request)
.then(response => response.json())
.then(data => {
let pages = data;
input.addEventListener("input",function(){
let filteredPages = pages;
results.innerHTML = "";
if (input.value != ""){
// Normalize and replace diacritics so searching for 'ramayana' will return matches for 'Rāmāyaṇa' etc
let searchterms = input.value.normalize("NFD").replace(/[\u0300-\u036f]/g,"").toLowerCase().split(" ");
// Apply a filter to the array of pages for each search term
searchterms.forEach(function(term) {
filteredPages = filteredPages.filter(function(page) {
// The description is the full object, includes title, tags, categories, and summary text
// You could make this more specific by doing something like:
// let description = page.title;
let description = JSON.stringify(page);
return description.normalize("NFD").replace(/[\u0300-\u036f]/g,"").toLowerCase().indexOf(term) !== -1;
});
}); // end of filter for loop
// For each of the pages in the final filtered list, insert into the results list
filteredPages.forEach(function(page) {
results.insertAdjacentHTML("beforeend","<li class='elbi-results-item'><h2 style='font-size: 1.5rem;'><a href='" + page.permalink + "'>" + page.title + "</a></h2><p>" + page.summary + "</p><p style='margin-top: 5px'>Tagged: <strong>" + page.tags + "</strong></p></li>");
}); // end of page for loop
}; // end of IF
}); // end of event listener
});
</script>
With any luck, that’s it! There’s practically no configuration required, although you’ll probably want to style it, so I’ve included some classes so that you can target it easily.
Hope it helps!