Build a client-side search with open_in_new Lunr . Lunr needs a JSON list of documents (title + body). Hugo can emit that list.
JSON index layout
layouts/_default/search.json:
{
"results": [
{{- range $index, $page := .Site.RegularPages }}
{{- if $index -}} , {{- end }}
{
"href": {{ .Permalink | jsonify }},
"title": {{ .Title | jsonify }},
"body": {{ .Content | plainify | jsonify }}
}
{{- end }}
]
}
Search content page
---
title: "Search"
draft: false
outputs:
- HTML
- JSON
layout: search
---
After build: /search/index.json.
Search layout
layouts/_default/search.html:
{{ define "main" }}
<h2>{{ .Title }}</h2>
<input type="search" id="searchField">
<button id="searchButton">Search</button>
<input id="allwords" type="checkbox">
<label for="allwords">Require all words</label>
<div id="output">
<p>Waiting for search input</p>
</div>
<script src="//unpkg.com/lunr@2.3.6/lunr.js"></script>
<script src="//unpkg.com/axios@0.19.0/dist/axios.js"></script>
<script src="{{ "js/search.js" | relURL }}"></script>
{{ end }}
search.js
'use strict'
window.SearchApp = {
searchField: document.getElementById("searchField"),
searchButton: document.getElementById("searchButton"),
allwords: document.getElementById("allwords"),
output: document.getElementById("output"),
searchData: {},
searchIndex: {}
};
axios
.get('/search/index.json')
.then(response => {
SearchApp.searchData = response.data;
SearchApp.searchIndex = lunr(function () {
this.pipeline.remove(lunr.stemmer);
this.searchPipeline.remove(lunr.stemmer);
this.ref('href');
this.field('title');
this.field('body');
response.data.results.forEach(e => {
this.add(e);
});
});
});
SearchApp.searchButton.addEventListener('click', search);
function search() {
let searchText = SearchApp.searchField.value;
searchText = searchText
.split(" ")
.map(word => { return word + "*" })
.join(" ");
if (SearchApp.allwords.checked) {
searchText = searchText
.split(" ")
.map(word => { return "+" + word })
.join(" ");
}
let resultList = SearchApp.searchIndex.search(searchText);
let list = [];
resultList.map(entry => {
SearchApp.searchData.results.filter(d => {
if (entry.ref == d.href) {
list.push(d);
}
})
});
display(list);
}
function display(list) {
SearchApp.output.innerText = '';
if (list.length > 0) {
const ul = document.createElement("ul");
list.forEach(el => {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = el.href;
a.text = el.title;
li.appendChild(a);
ul.appendChild(li);
});
SearchApp.output.appendChild(ul);
} else {
SearchApp.output.innerHTML = "Nothing found";
}
}
Trailing * enables prefix matching; + requires every word when the checkbox is on. Stemming is disabled for more predictable matches.
See also custom JSON output for section-level JSON feeds.
Andrew Dorokhov