Tutorial Menambahkan Fitur Search di Hexo

Bila sebelumnya saya sudah pernah membahas tentang cara menambahkan fitur search di Hugo, kali ini kita akan membahas untuk SSG Hexo. Hexo sendiri punya built-in helper untuk menambahkan Google Search form dan beberapa plugin untuk menambahkan form pencarian menggunakan Algolia, Baidu, dan sebagainya.
Sayangnya, kebanyakan plugin yang tersedia tidak update, sedangkan Google Search form agak sulit dikelola karena menggunakan iframe. Maka, kita akan menggunakan library fuse.js lagi.
Cara Menambahkan Search Form di Hexo
Meskipun fitur pencarian yang akan kita buat menggunakan library JavaScript yang sama, tetapi karena antara Hugo dan Hexo memiliki struktur folder serta menggunakan template engine yang berbeda, maka ada beberapa langkah dan sintaks yang berbeda pula.
1. Membuat Database
Ok, first thing first, kita membutuhkan semacam database. Untuk itu, saya sudah membuat artikel khusus, silakan buat database dengan mengikuti langkah-langkah dalam artikel Cara Membuat Database di Hexo.
File post-data.json
di folder public/data
inilah yang akan digunakan sebagai sumber data yang dapat dicari dan ditampilkan.
2. Buat Search Form
Berikutnya, buat search form di mana pun Anda ingin form ini ditampilkan. Lalu, masukkan kode HTML berikut:
<form action="/search/" method="GET">
<input type="search" name="q" id="search-query" placeholder="Search...." />
<button type="submit">Search</button>
</form>
Saya sendiri mengaturnya dengan cara begini:
- Search form disembunyikan.
- Buat 1 elemen
button
berupa ikon search dan meletakkannya di header. - Ketika elemen
button
tersebut diklik, search form akan ditampilkan.
3. Page: source/search/index.md
Buat halaman pencarian dengan perintah hexo new page "Search"
. Maka Hexo akan men-generate sebuah folder search
lengkap dengan file index.md
di folder source
.
Buka file index.md
dan tambahkan layout
di front-matter.
---
title: Search
date: 2024-12-30 13:18:44
layout: search-layout
---
Anda tak perlu menambahkan apa-apa lagi karena hasil pencarian akan ditangani oleh layout search yang akan kita buat di langkah berikutnya.
4. Layout: themes/themes-anda/layout/search-layout.ejs
Halaman inilah yang akan menampilkan hasil pencarian. Buat file search-layout.ejs
di folder ./themes/theme-anda/layout/search-layout.ejs
.
<section>
<h1 id="search-heading">Search Result for</h1>
<!-- Hasil pencarian akan ditampilkan di sini -->
<div id="search-results"></div>
<nav id="search-pagination" aria-label="Pagination Navigation"></nav>
<div class="search-loading" style="display: none;">Loading...</div>
<!-- Template untuk mengatur tampilan hasil pencarian -->
<script id="search-result-template" type="text/x-js-template">
<article id="summary-${key}">
<figure>
<img src="${image}" alt="${title}">
</figure>
<div>
<h2><a href="/${link}">${title}</a></h2>
<p>${snippet}</p>
</div>
</article>
</script>
</section>
Seperti biasa, saya menghilangkan class
, Anda dapat menambahkan class
pada template dan mengatur tampilannya dengan CSS. Bila menggunakan Tailwind, langsung saja tambahkan class
-nya pada tiap elemen.
5. JavaScript: themes/theme-anda/source/search.js
Buat file search.js
di folder ./themes/theme-anda/source
. Saya sendiri meletakkan semua file JavaScript di folder ./themes/langit-theme/source/js
.
File inilah yang berisi konfigurasi fuse.js. Masukkan script berikut:
const summaryInclude = 100;
const resultsPerPage = 12; // Jumlah hasil per halaman
let currentPage = 1; // Halaman saat ini
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: "content", weight: 0.1 },
{ name: "tags", weight: 0.1 },
{ name: "categories", weight: 0.05 },
],
};
const inputBox = document.getElementById("search-query");
const searchHeading = document.getElementById("search-heading");
if (inputBox !== null) {
let searchQuery = param("q");
if (searchQuery) {
inputBox.value = searchQuery || "";
executeSearch(searchQuery);
}
}
function executeSearch(searchQuery) {
fetch("/data/post-data.json")
.then(response => response.json())
.then(data => {
const fuse = new Fuse(data, fuseOptions);
const result = fuse.search(searchQuery);
// Batasi hasil pencarian hingga 12 item per halaman
const totalResults = result.length;
const totalPages = Math.ceil(totalResults / resultsPerPage);
const paginatedResults = result.slice((currentPage - 1) * resultsPerPage, currentPage * resultsPerPage);
// Perbarui heading dengan query pencarian
searchHeading.textContent = `Search Result for "${searchQuery}"`;
populateResults(paginatedResults, totalResults, totalPages);
})
.catch(err => console.error("Error fetching search data:", err));
}
function populateResults(results, totalResults, totalPages) {
const searchResults = document.getElementById("search-results");
const pagination = document.getElementById("search-pagination");
const template = document.getElementById("search-result-template").innerHTML;
if (results.length > 0) {
searchResults.innerHTML = results.map(result => {
const item = result.item;
const snippet = item.content.substring(0, summaryInclude) + "...";
const tags = item.tags.join(", ");
const output = render(template, {
key: item.key,
title: item.title,
link: item.path,
tags: tags,
categories: item.categories,
snippet: snippet,
image: item.image,
});
return output;
}).join("");
// Tambahkan navigasi pagination jika lebih dari satu halaman
if (totalPages > 1) {
pagination.innerHTML = createPagination(totalPages);
} else {
pagination.innerHTML = "";
}
} else {
searchResults.innerHTML = "<p>No results found</p>";
pagination.innerHTML = "";
}
}
function createPagination(totalPages) {
let paginationHTML = "";
for (let i = 1; i <= totalPages; i++) {
paginationHTML += `<button class="pagination-button" data-page="${i}">${i}</button>`;
}
return paginationHTML;
}
function render(template, data) {
return template.replace(/\${(.*?)}/g, (match, key) => data[key.trim()]);
}
// Event listener untuk navigasi pagination
document.addEventListener("click", function(event) {
if (event.target.classList.contains("pagination-button")) {
currentPage = parseInt(event.target.getAttribute("data-page"));
const searchQuery = inputBox.value;
executeSearch(searchQuery);
}
});
// 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((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [,""])[1].replace(/\+/g, '%20')) || null;
}
Di sini saya menggunakan pagination dengan jumlah hasil per halaman 12. Anda dapat mengubah jumlahnya sesuai dengan yang Anda inginkan. Karena ini diinisiasi oleh JavaScript di client-side, kita tidak dapat menggunakan fitur pagination bawaan Hexo. Agar tampilannya serupa, atur-atur aja CSS-nya.
Bila Anda bingung antara file JavaScript di folder themes/theme-anda/scripts
dengan yang di folder themes/theme-anda/source
, anggap saja begini:
- Di folder
scripts
: backend. File JavaScript yang akan dieksekusi di local server. Di sini kita menempatkan file untuk helper, generate data, tag plugins, dll. Semua file di sini tidak akan di-render. - Di folder
source
: front-end. File JavaScript yang akan dijalankan di client side. Misalnya untuk DOM manipulation, fitur pencarian, dll.
6. Masukkan Fuse.js ke Layout
Masukkan fuse.js
dan search.js
ke layout. Buka file layout.ejs
di folder ./themes/theme-anda/layout
kemudian masukkan script berikut sebelum tag </body>
.
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0" defer></script>
<%- js('js/search-script.js') %>
Setelah selesai, coba lakukan pencarian dan lihat apakah hasil pencarian sudah tampil atau belum. (eL)