Fitur search atau pencarian merupakan salah satu elemen penting dalam sebuah website. Fitur ini memungkinkan pengunjung untuk mencari artikel yang ingin mereka baca. Selain memudahkan mereka, fitur pencarian juga bermanfaat untuk meningkatkan pengalaman pengguna.
Lalu, bagaimana caranya menambahkan fitur search di Hugo?
Meskipun Hugo adalah Static-Site Generator (SSG) yang secara default tidak memungkinkan untuk fungsi pencarian, kita bisa “mengakali” itu dengan menggunakan library JavaScript dari pihak ketiga. Di artikel ini, kita akan sama-sama membuat fitur pencarian di Hugo menggunakan library Fuse.js dan Mark.js.
Mengapa Fuse.js?
Ada beberapa alasan mengapa saya memutuskan untuk menggunakan Fuse.js alih-alih library lainnya.
Fuse.js adalah sebuah library JavaScript untuk fuzzy searching. Fuzzy searching adalah sebuah metode pencarian untuk menemukan string yang kurang lebih sama dengan pola tertentu. Pencarian Fuse.js dilakukan di sisi klien (client-side) atau di browser sehingga tidak memerlukan database. Hal ini tentu kompatibel dengan Hugo yang memang tidak memiliki backend.
Selain itu, Fuse.js juga ringan dan tanpa dependensi. Bahkan, kita bisa menggunakan Fuse.js dengan atau tanpa jQuery.
Bagaimana cara kerjanya? Kita akan membuat file index.json
yang men-generate semua konten web kita ke dalam file json
. File json
inilah yang digunakan Fuse.js sebagai “database” untuk mencari konten berdasarkan query yang diketikkan pengguna lalu menampilkan hasilnya di search result.
Cara Menambahkan Fitur Search di Hugo dengan Fuse.js dan Mark.js
Sebagai bayangan, kita akan membuat fitur pencarian yang menghasilkan search result seperti ini.
Langkah-langkah dan sintaks saya sadur dari artikel Add Search to a Hugo Site dengan beberapa penyesuaian. Seperti misalnya, di artikel tersebut hasil pencariannya tidak ada gambar atau featured image, saya menambahkan featured image agar tampilan hasil pencarian mirip dengan tampilan post index di blog ini.
Di artikel tersebut, Edd juga masih menggunakan Fuse.js v.6.2.2, sedangkan saya menggantinya ke versi paling baru ketika artikel ini ditulis yaitu v.7.0.0.
Oh ya, saya menggunakan vanilla JavaScript. Untuk Anda yang menggunakan jQuery, bisa membaca dokumentasi di Gist . Jangan lupa untuk menggunakan Fuse.js dan Mark.js versi terbaru.
1. Buat Form Search
Langkah pertama adalah membuat form untuk pencarian. Letakkan di mana pun Anda ingin form pencarian itu ditampilkan. Secara sederhana, form-nya adalah seperti ini:
<form action="/search" method="GET">
<input type="search" name="q" id="search-query" placeholder="Search...." />
<button type="submit">Search</button>
</form>
Anda bisa menambahkan class
pada form, input, atau button untuk mengatur tampilan. Bisa juga mengganti button dengan ikon. Tetapi ada beberapa hal yang perlu diperhatikan dan harus persis sama:
action="/search"
==> atributaction
di sini mengacu pada halamansearch.html
yang nanti akan kita buat.method="GET"
==> method-nya get.name="q"
==> params.id="search-query"
==>id
ini akan kita gunakan untuk inisialisasi di JavaScript.
2. Page: /content/search/_index.md
Buat folder search
di content
. Kemudian di dalamnya buat file _index.md
. Direktori lengkapnya: content/search/_index.md
.
Di dalam file _index.md
tambahkan script berikut:
---
title: "Search"
sitemap:
priority: 0.1
layout: "search"
---
Kita tak perlu menambahkan apa-apa lagi di _index.md
. Tampilan hasil pencarian akan di-handle oleh file search.html
yang akan kita buat di langkah ketiga.
3. Layout: /layouts/_default/search.html
Buat file search.html
di layouts/_default/search.html
. Anda bisa juga membuatnya di dalam folder themes sehingga direktorinya menjadi themes/layouts/_default/search.html
. Halaman inilah yang nanti akan menangani hasil pencarian.
Masukkan script berikut:
<!-- layouts/_default/search.html -->
{{ define "main" }}
<div>
<h1 id="search-heading">Search Result for</h1>
<!-- Hasil pencarian akan ditampilkan di sini -->
<div id="search-results"></div>
<div class="search-loading">Loading...</div>
<!-- Template untuk mengatur tampilan hasil pencarian -->
<script id="search-result-template" type="text/x-js-template">
<article id="summary-${key}">
<div><img src="${featured_image}" alt="${title}"></div>
<div>
<h2><a href="${link}" class="absolute-link">${title}</a></h2>
<p>${snippet}</p>
<small>
${ isset tags }Tags: ${tags}<br>${ end }
</small>
</div>
</article>
</script>
</div>
<!-- File JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js"></script>
<script src="/js/search.js"></script>
{{ end }}
Saya sengaja menghilangkan class
dari semua elemen, Anda bebas menambahkan class
untuk mengatur tampilan dengan CSS
agar sesuai dengan blog Anda. Yang terpenting id
dalam elemen yang nanti akan kita gunakan di JavaScript. Anda juga bisa memindahkan <script>
ke lokasi lain, misalnya sebelum </body>
.
4. JavaScript: static/js/search.js
Buat file JavaScript dengan nama file search.js
di folder static
.
const summaryInclude = 100;
const fuseOptions = {
shouldSort: true,
includeMatches: false,
findAllMatches: false,
includeScore: true,
tokenize: true,
location: 0,
ignoreLocation: false,
threshold: 0.3,
distance: 1000,
maxPatternLength: 32,
minMatchCharLength: 2,
keys: [
{ name: "title", weight: 1 },
{ name: "contents", weight: 0.1 },
{ name: "tags", weight: 0.1 },
{ name: "categories", weight: 0.05 },
],
};
// =============================
// Search
// =============================
const inputBox = document.getElementById("search-query");
if (inputBox !== null) {
let searchQuery = param("q");
if (searchQuery) {
inputBox.value = searchQuery || "";
executeSearch(searchQuery, false);
} else {
const searchHeading = document.getElementById("search-heading");
const searchResults = document.getElementById("search-results");
// Pemeriksaan keberadaan elemen sebelum mengubah propertinya
if (searchHeading !== null) {
searchHeading.innerHTML = `No Results Found`;
}
if (searchResults !== null) {
searchResults.innerHTML = `<p class="font--center search-result-empty">Please enter a word or phrase in the search form.</p>`;
}
}
hide(document.querySelector(".search-loading"));
}
function executeSearch(searchQuery) {
show(document.querySelector(".search-loading"));
fetch("/index.json").then(function (response) {
if (response.status !== 200) {
console.log(
"Looks like there was a problem. Status Code: " + response.status,
);
return;
}
// Examine the text in the response
response
.json()
.then(function (pages) {
const fuse = new Fuse(pages, fuseOptions);
const result = fuse.search(searchQuery);
if (result.length > 0) {
document.getElementById("search-heading").innerHTML =
`Search Result for "${searchQuery}"`;
populateResults(result);
} else {
document.getElementById("search-heading").innerHTML =
`Search Result for "${searchQuery}"`;
document.getElementById("search-results").innerHTML =
'<p class="font--center search-result-empty">No matches found</p>';
}
hide(document.querySelector(".search-loading"));
})
.catch(function (err) {
console.log("Fetch Error :-S", err);
});
});
}
function populateResults(results) {
const searchQuery = document.getElementById("search-query").value;
const searchResults = document.getElementById("search-results");
// pull template from hugo template definition
const templateDefinition = document.getElementById(
"search-result-template",
).innerHTML;
results.forEach(function (value, key) {
const contents = value.item.contents;
let snippet = "";
const snippetHighlights = [];
snippetHighlights.push(searchQuery);
snippet = contents.substring(0, summaryInclude * 2) + "…";
//replace values
let tags = "";
if (value.item.tags) {
value.item.tags.forEach(function (element) {
const encodedTag = element.toLowerCase().replace(/ /g, "-");
tags =
tags +
"<a href='/blog/tags/" +
encodedTag +
"'>" +
"#" +
element +
"</a> ";
});
}
const output = render(templateDefinition, {
key: key,
title: value.item.title,
link: value.item.permalink,
tags: tags,
categories: value.item.categories,
snippet: snippet,
featured_image: value.item.featured_image,
});
searchResults.innerHTML += output;
snippetHighlights.forEach(function (snipvalue, snipkey) {
const instance = new Mark(document.getElementById("summary-" + key));
instance.mark(snipvalue);
});
});
}
function render(templateString, data) {
let conditionalMatches, conditionalPattern, copy;
conditionalPattern = /\$\{\s*isset ([a-zA-Z]*) \s*\}(.*)\$\{\s*end\s*}/g;
//since loop below depends on re.lastInxdex, we use a copy to capture any manipulations whilst inside the loop
copy = templateString;
while (
(conditionalMatches = conditionalPattern.exec(templateString)) !== null
) {
if (data[conditionalMatches[1]]) {
//valid key, remove conditionals, leave contents.
copy = copy.replace(conditionalMatches[0], conditionalMatches[2]);
} else {
//not valid, remove entire section
copy = copy.replace(conditionalMatches[0], "");
}
}
templateString = copy;
//now any conditionals removed we can do simple substitution
let key, find, re;
for (key in data) {
find = "\\$\\{\\s*" + key + "\\s*\\}";
re = new RegExp(find, "g");
templateString = templateString.replace(re, data[key]);
}
return templateString;
}
// Helper Functions
function show(elem) {
if (elem) {
elem.style.display = "block";
}
}
function hide(elem) {
if (elem) {
elem.style.display = "none";
}
}
function param(name) {
return decodeURIComponent(
(location.search.split(name + "=")[1] || "").split("&")[0],
).replace(/\+/g, " ");
}
Apabila Anda ingin memodifikasi const fuseOptions
lebih jauh, bisa baca-baca di dokumentasi Fuse.js
.
5. Json: layouts/_default/index.json
Sekarang, kita buat file index.json
di /layouts/_default/index.json
. Seperti yang saya sampaikan sebelumnya, file inilah yang akan berfungsi sebagai “database”. Masukkan kode berikut.
{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
{{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "categories" .Params.categories "contents" .Plain "permalink" .Permalink "featured_image" .Params.featured_image) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
6. Config: hugo.toml
Langkah berikutnya adalah menambahkan [outputs]
di file konfigurasi. Buka file hugo.toml
lalu tambahkan script berikut.
[outputs]
home = ["HTML", "RSS", "JSON"]
Apabila Anda masih menggunakan Hugo di bawah v.0.110.0, barangkali Anda masih menggunakan file config.toml
. Saya kira ini waktunya untuk melakukan update Hugo agar konfigurasinya lebih update.
Konflik dengan Cusdis
Karena saya menggunakan sistem komentar Cusdis
, terjadi konflik dengan pesan eror “Uncaught SyntaxError: Identifier ’e’ has already been declared”. Itu karena Cusdis dan Fuse.js sama-sama menggunakan variabel e
.
Meskipun saya berusaha sebisa mungkin menggunakan block scope, entah kenapa eror tetap terjadi. Solusinya adalah mengganti nama variabel di Cusdis. Bila Anda tidak menggunakan Cusdis atau JavaScript lain yang menggunakan nama variabel sama, saya kira eror ini tidak akan terjadi.
Yak, selesai! Silakan tes fitur search yang telah Anda buat. Semoga tidak ada eror, ya. Apabila Anda mengalami kendala atau punya saran tambahan, silakan tuliskan di kolom komentar. (eL)
Langit Amaravati
Web developer, graphic designer, techno blogger.
Aktivis ngoding barbar yang punya love-hate relationship dengan JavaScript. Hobi mendengarkan lagu dangdut koplo dan lagu campursari. Jika tidak sedang ngoding dan melayout buku, biasanya Langit melukis, belajar bahasa pemrograman baru, atau meracau di Twitter.