Hugo Search by Title using Plain JavaScript

The search form code (add it to /layouts/partials/ folder as search-form.html). Filter the $pages variable to suit your needs (e.g. by type, by section, by date, reverse, etc.).

Summary
  {{ $pages := where site.RegularPages "Section" "==" "blog" }}
  {{ $format := "2006-01-02"}}

  <input
      class="form-control"
      id="search"
      type="text"
      aria-label="Search by title"
      placeholder="Loading..."
      disabled
  >

  <p id="count">
    <span id="count-label">
      <strong>Count:</strong>
    </span>
    <span id="count-value">
      {{ len $pages }}
    </span>
  </p>

  <div id="list" class="">
    {{ range $pages }}
      <p>
        <span class="post-date">
          {{ .PublishDate.Format $format }}
        </span>
        <br>
        <span class="post-title">
          <a href="{{ .RelPermalink }}">{{ .Title }}</a>
        </span>
      </p>
    {{ end }}
  </div>
</div>

The JavasCript Code (save the file as search.js in assets/js/ folder)

Summary
(function () {
  const LOG_ENABLED = false;

  const logObj = (objNameStr, obj) => {
    if (LOG_ENABLED) {
      console.log(objNameStr, JSON.stringify(obj));
    }
  };

  const logPerformance = (funcNameStr, func) => {
    const startTimeNum = performance.now();
    func();
    const endTimeNum = performance.now();
    const durationStr = (endTimeNum - startTimeNum).toFixed(2);

    if (LOG_ENABLED) {
      console.log(`${funcNameStr} took ${durationStr} ms`);
    }
  };

  const getSearchEl = () => {
    return document.querySelector('#search');
  };

  const getCountEl = () => {
    return document.querySelector('#count-value');
  };

  const getPostEls = () => {
    return document.querySelectorAll('#list p');
  };

  const getQueryWordsArr = () => {
    return getSearchEl().value.trim().toUpperCase().split(' ');
  };

  const getPostTitleStr = postEl => {
    return postEl
      .querySelector('span.post-title')
      .textContent.trim()
      .toUpperCase();
  };

  const isHit = (queryWordsArr, titleStr) => {
    return queryWordsArr.every(queryWordStr => {
      return titleStr.includes(queryWordStr);
    });
  };

  const showPost = postEl => {
    postEl.style.display = 'block';
  };

  const hidePost = postEl => {
    postEl.style.display = 'none';
  };

  const updateCountEl = countNum => {
    getCountEl().textContent = countNum;
  };

  const filterPosts = () => {
    const queryWordsArr = getQueryWordsArr();
    const postEls = getPostEls();
    let countNum = postEls.length;

    postEls.forEach(postEl => {
      const titleStr = getPostTitleStr(postEl);
      const hit = isHit(queryWordsArr, titleStr);

      logObj('queryWordsArr', queryWordsArr);
      logObj('titleStr', titleStr);

      if (hit) {
        showPost(postEl);
      } else {
        hidePost(postEl);
        countNum--;
      }
    });

    updateCountEl(countNum);
  };

  const handleKeyupEvent = () => {
    logPerformance('filterPosts', filterPosts);
  };

  const enableSearchEl = () => {
    getSearchEl().disabled = false;
    getSearchEl().placeholder = 'Search by title';
  };

  const main = () => {
    getSearchEl().addEventListener('keyup', handleKeyupEvent);
    enableSearchEl();
  };

  main();
})();

Create a search.html layout for your search page and store it in layouts/_default. An example

Summary
{{ define "main"}}
<header>
   <h1>{{ .Title }}</h1>
</header>
<article>
   {{ partial "search-form.html" . }}
</article>
{{ end }}

Create a search.md page in your content folder and include your layout. An example

Summary
---
title: Search Page
url: /search/
layout: search
---

Call the script in your baseof.html file before the closing </body> tag. An example

Summary
  {{- if hasPrefix .RelPermalink "/search/"}}
  {{- $search := resources.Get "js/search.js" }}
  <script src="{{ $search.RelPermalink }}" defer></script>
   {{- end }}

There are other ways to achieve this. So, feel free to adapt this to your needs.

Credit: This solution is a modified version originally by @zwbetz I found here. See it in action.

4 Likes

This looks familiar :slightly_smiling_face:

Thanks for posting and glad to hear you were able to adapt it to your needs :+1:

1 Like

Finally found your username! I was on your site a few days ago and saw the search. I went down the rabbit hole of finding you on GitHub and piecing the pieces together, since your implementation is a bit complex. Took me a few hours. But it is fantastic!

1 Like