Tutorial Menambahkan Fitur Search di Hexo

Menambahkan fitur search di Hexo
Image: generated with AI

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.

Notes:

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)

TAGS: Hexo
Langit Amaravati

Langit Amaravati

Web developer, graphic designer, techno blogger.

Suka dengan artikel-artikel di blog ini dan merasa mendapatkan manfaatnya? Dukung saya dengan mentraktir kopi. Dengan dukungan Anda, saya dapat terus menulis dan berkarya.

Hatur nuhun!

Traktir Kopi