Hugo - статический генератор сайтов, но что же делать если хочется добавить интерактивный элемент? Система поиска - важная, а зачастую и необходимая функция на сайте, но написать её используя только Hugo попросту не получиться, исходя из природы инструмента. Потому придётся примкнуть к тёмной стороне и вспомнить JS...
Идея
Система поиска будет использовать JSON файл как поисковый индекс, где будет храниться информация о страницах (regular page, ака single). Чтобы самостоятельно не вбивать информацию о каждой новой странице, файл должен сам генерировать нужную нам структуру.
Страница с форматом JSON
Для начала нужно объявить в конфиге (config.toml) страницу, которая будет иметь формат JSON. Самый лёгкий вариант - главная. Кстати говоря, это не уничтожит вашу home page, т.е. она будет также отображаться следуя вашему index.html.
config.toml
[outputs]
home = ["HTML", "JSON"]
Поисковый индекс
Теперь создадим самогенерирующийся JSON файл, где будет храниться нужная нам информация по каждой странице. Создадим файл index.json (в той же директории, где находится index.html, в моём случае - layouts).
index.json
{{ $number := 0 -}}
{{ $pages := len .Site.RegularPages }}
[
{{- range.Site.RegularPages -}}
{{- $number = add $number 1 -}}
{
"title": "{{ .Title }}",
"url": "{{ .RelPermalink }}",
"plain": "{{ .PlainWords }}",
"parent": "{{ .Parent.Title }}"
}
{{- if and (ge $number 1) (lt $number $pages) }},{{ end -}}
{{- end }}
]
Раскроем что творит это детище. Мы проходим по всем обычным страницам сайта (regular page, ака single) со строчки 4, и выдёргивает нужные нам параметры (title - название, url - ссылка, plain - содержание страницы, parent - директория в которой находится страница). $number и $pages вместе с if and - нужны, чтобы расставить запятые между элементами JSON, не более.
Оформляем search bar
Создадим контейнер для поля поиска и результатов поиска.
index.html
<div class="search-wrapper">
<h1 style="text-align:center">Ищешь что-то?</h1>
<input class="input" id="searchInput" type="text" placeholder="Поиск...">
<div class="searchResults">
<div id="searchResult"></div>
</div>
</div>
Для примера прилагаю также свой CSS файл. Не жалко.
style.css
.search-wrapper {
text-align: center ;
padding: 15px;
}
.input {
border: 1px solid #373b42;
border-radius: 5px;
height: 25px;
width: 30%;
font-size: 14px;
padding: 2px 23px 2px 30px;
background-color: #21262d;
color: #ffffff;
font-family: JetBrains Mono, regular;
}
.input:hover, .input:focus {
border: 1px solid #58a6ff;
}
.searchResults {
position: absolute;
left: 46%;
width: 23%;
top: 25%;
background-color: #0d1117;
border: 1px solid #373b42;
}
.search-result-item {
padding: 20px;
}
.search-result-item a {
text-decoration: none;
color: #58a6ff;
}
Теперь пришло время обратиться к нашему лучшему другу - JS. Нам надо, чтобы при каком-либо действии запускалась генерация JSON файла (aka нашего поискового индекса). Легче всего это сделать при фокусировке пользователя на поле ввода запроса на поиск.
Напишем функцию "запуска" JSON.
const GetPostsJSON = async () =>
{
let response = await fetch('/index.json')
let data = await response.json()
return data
}
А также функцию фильтрации результатов на основе запроса. Эта же функция будет "добавлять" HTML кода для отображения результатов в нашем контейнере.
const filterPostsJSON = (query, element) =>
{
let result, itemsWithElement;
query = new RegExp(query, 'ig')
result = dataJSON.filter(item => query.test(item.title) | query.test(item.plain))
itemsWithElement = result.map(item => (
`<div class="search-result-item">
<a href="${item.url}">
${item.parent} / ${item.title}
<span class="icon">
<i class="fas fa-external-link-alt"></i>
</span>
</a>
</div>`
))
element.style.display = 'block';
element.innerHTML = itemsWithElement.join('');
}
В данном случае я проверяю на совпадение в имени странице (item.title) и содержании страницы (item.plain).
Далее остаётся только добавить обработку events.
const searchInputAction = (event, callback) =>
{
searchInput.addEventListener(event, callback)
}
// - При фокусировании на поиске вызывать getPostsd
searchInputAction('focus', () => getPostsJSON().then(data => dataJSON = data))
// - Фильтровать результаты по запросу
searchInputAction('keyup', (event) => filterPostsJSON(event.target.value, searchResult))
Дело в шляпе
Вот так, с помощью щепотки JS можно добавить простую систему поиска на Hugo сайт. Оно вам не эластик сёрч, но для небольших решений подходит отлично.
Если тебе интересно узнать про системы посложнее, рекомендую обратиться к данному проекту.