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 сайт. Оно вам не эластик сёрч, но для небольших решений подходит отлично.

Если тебе интересно узнать про системы посложнее, рекомендую обратиться к данному проекту.

Комментарии (8)


  1. urvanov
    23.07.2022 23:48

    А нельзя просто отсылать в гугл добавляя site:myste.com <поисковый_запрос>? Раньше на народе так делали, вроде.


    1. dolfinus
      24.07.2022 11:42

      Гугл же не мгновенно индексирует страницы


  1. FooBarBuzz
    25.07.2022 04:07

    Не проводили тестирование? Сколько страниц должно быть на сайте, чтобы поиск заметно подмораживал?


    1. dacsson Автор
      25.07.2022 04:09

      Идея хорошая, спасибо,сам недопëр, надо будет затестить.


  1. VladimirVs
    25.07.2022 04:07

    Спасибо за статью. Может очень пригодиться когда нужно работать без подключения к интернету, и до Гугла не добраться.


    1. dacsson Автор
      25.07.2022 04:10

      Рад помочь!


  1. romankurnovskii
    25.07.2022 04:07

    Спасибо за статью. Столкнулся с проблемой для мультиязычного сайта. Данная реализация добавит все языки в один индекс без возможности их фильтра при поиске.

    Выложил в выходные пакет https://www.npmjs.com/package/hugo-lunr-ml, который создает индексы для каждого языка.


    1. dacsson Автор
      25.07.2022 04:10

      Рад, что помог. Пакет очень полезный обязательно им попользуюсь.