Issue
You want to add a basic search functionality to your Hugo website, however the suggestions from the official documentation feels a bit too much for your needs. Like if you want to search by one parameter only, or you don’t want to install additional packages, then this quick tutorial is a decent way to achieve this.
Solution
Full Disclosure on Hugo: I perhaps have less than 24 actual hours of experience with Hugo, just used a theme and modified it a bit to get what I want. I apologize in advance as how I do things here aren’t best practice in Hugo.
- In your theme’s
layouts > partials
folder, create the html that houses the search input and the results (I’m not even sure if this folder structure is standard).
<!--The css styles can be ignored.-->
<div id="search-container" class="columns">
<div class="column is-three-quarters-desktop">
<input placeholder="Search all posts..." id="search-input" class="input"/>
<div id="search-result">
</div>
</div>
</div>
- Inside the
layouts > _default
folder, create an index.json, this will have the data we will search client-side
{{ $.Scratch.Add "index" slice }}
{{ range where .Site.RegularPages "Section" "blog" }}
{{ $.Scratch.Add "index" (dict "title" .Title "permalink" .Permalink "tags" .Params.tags ) }}
{{ end }}
{{ $.Scratch.Get "index" | jsonify }}
Note: We can get all the regular pages there on the 2nd line, or filter the range to something else. As for the index, add only the values you want to search (title, link and tags for my case)
, to keep it as small as possible
- In your
config.toml
, tell your home page to output json as well
[outputs]
home = ['html','json']
- In your
assets
folder, add the JavaScript file that will do the searching
(() => {
let searchIndex = null;
// populate the search index object
const request = new XMLHttpRequest();
request.onreadystatechange = () => {
// 4 - done request, 200 - OK
if (request.readyState === 4 && request.status === 200) {
searchIndex = JSON.parse(request.responseText);
}
}
request.open('GET', '/index.json');
request.send();
const search = async (query, index) => {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
const regex = new RegExp(query.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const keys = ['permalink','tags','title']; //can add other keys here if wanted
// get only the stuff that matches the regex pattern
const result = index.filter((value) => {
for (const key of keys) {
if (!value[key]) continue;
if (Array.isArray(value[key])){
// search each value in the nested array
if (value[key].some(v => regex.test(v.toLowerCase()))) return true;
}else if(regex.test(value[key].toLowerCase())) return true;
}
return false;
});
// put the results in a list that's rendered on the screen
if (result.length > 0) {
const ul = document.createElement('ul');
ul.setAttribute('class','pt-1');
result.forEach(item => {
const li = document.createElement('li');
const aTag = document.createElement('a');
li.setAttribute('class','px-1 py-1');
aTag.setAttribute('href', item.permalink);
aTag.setAttribute('class', 'is-block');
aTag.innerHTML = item.title;
li.appendChild(aTag);
ul.appendChild(li);
})
return ul
}
return null;
}
const searchResult = document.getElementById('search-result');
const searchInput = document.getElementById('search-input');
// search on input
searchInput?.addEventListener('input', async (e) => {
let result = null;
if (e.currentTarget.value) result = await search(e.currentTarget.value, searchIndex);
if (searchResult.lastChild) searchResult.removeChild(searchResult.lastChild);
if (result) searchResult.appendChild(result);
});
// clear the search input - this is so when you press back in your browser,
// the search input is cleared nicely
searchInput?.addEventListener('focusout', (e) => {
e.target.value = '';
// timeout is there because removing the element instantly won't let you hit the anchor tag
setTimeout(() => {
if (searchResult.lastChild) searchResult.removeChild(searchResult.lastChild);
}, 150);
});
})();
- Then call this script somewhere in your homepage, in my case it is on another partial page
<!-- At the home page -->
{{ partial "footer/scripts.html" . }}
<!-- At the partial page. layouts > partials > footer folder -->
{{ range $value := .Site.Params.customJS }}
{{ $script := resources.Get . | js.Build (dict "minify" true) }}
<script src="{{ $script.Permalink }}"></script>
{{ end }}
Add your own styling to finish it off. Here is a working demo on my site’s blog area.
One downside I can imagine is when there are lots of pages in your site, the index file will be big, which will slow down this solution. If you also search the contents of each page, the index file can get bigger even faster. For small sites however (I’m guessing less than 5,000 pages without searching the page’s content), this solution should work pretty well.
Top comments (0)