Blog Setup (part 4) : Add Search

 

[!WARNING]

For my blog, I am no longer using the anatole theme showed in this post and series. I made a Tailwind CSS theme. Read about it in Hugo Theme Development Series

Introduction

Since Hugo generates a static site, the search has to be performed on the client side. Fuse.js is a lightweight library for approximate string matching that runs in the browser. It can take a json file, search for a term and return an array of objects where the matching string was found.

 

Installation

hugofastsearch is an script that implements Fuse.js in Hugo. Follow the steps in hugofastsearch gist:

 

Add search button

By default, hugofastsearch uses Ctrl + / to show the search input box. We can add the search button and input box HTML to themes/anatole/layouts/partials/navabar.html

<li class="nav__list-item">
  <div id="fastSearch">
    <input id="searchInput" tabindex="0">
    <ul id="searchResults">
    </ul>
  </div>
<li>
<li class="nav__list-item">
  <div id="SearchButton">
    <a title="Search">
      <i class="fas fa-search fa-fw" aria-hidden="true"></i>
    </a>
  </div>
</li>

Add css for search button, search input box and search results in themes/anatole/assets/custom.css

.nav__list--end>.nav__list-item {
    display: flex;
    align-items: center;
    align-content: center;
    justify-content: center;
}

#fastSearch { 
    visibility: hidden;
    max-width: 600px;
    white-space: normal;
    text-transform: none;
    text-align: justify;
    min-width: 22vw;
    position: relative;
}      

#fastSearch input {
    width: 100%;
    font-size: 1.2em;
    font-weight: 400;
    border-radius: 3px 3px 0 0;
    border: none;
    outline: none;
    text-align: left;
    display: inline-block;
    background-color: #ebebeb;
    padding: 10px;
    box-sizing: border-box;
}

#searchResults {
    visibility: inherit;
    position: absolute;
    display: block;
    margin: 0;
    padding: 0;
    min-width: 22vw;
}

.theme--light #searchResults {
    background-color: #fff;
}

.theme--dark #searchResults {
    background-color: #152028;
}

#searchResults li {
    list-style: none;
    background-color: #ebebeb;
    margin-top: 2px;
    width: 100%;
}

#searchResults a {
    text-decoration: none!important;
    padding: 10px 15px;
    display: block;
}

#searchResults li .title { 
    font-size: 1.1em;
    margin-bottom: 10px;
    display: inline-block;
}

.theme--dark #searchResults a {
    background-color: #20313c;
}

#searchResults a:hover,
#searchResults a:focus { 
    outline: 0; background-color: #9f9f9f; color: #fff; 
}

.theme--dark #searchResults a:hover,
.theme--dark #searchResults a:focus {
    background-color: #324c5d;
}

Modify fastsearch.js to add click events to document and search button. Move toggle code out of Ctrl + / keydown event and make a function with it called toggleSearch and create openSearch and closeSearch functions

function toggleSearch() {
  // Load json search index if first time invoking search
  // Means we don't load json unless searches are going to happen; keep user payload small unless needed
  if(firstRun) {
    loadSearch(); // loads our json data and builds fuse.js search index
    firstRun = false; // let's never do this again
  }

  // Toggle visibility of search box
  if (!searchVisible) {
    document.getElementById("fastSearch").style.visibility = "visible"; // show search box
    document.getElementById("searchInput").focus(); // put focus in input box so you can just start typing
    searchVisible = true; // search visible
  }
  else {
    document.getElementById("fastSearch").style.visibility = "hidden"; // hide search box
    document.activeElement.blur(); // remove focus from search box 
    searchVisible = false; // search not visible
  }
}

function openSearch() {
  if(!searchVisible) {
    toggleSearch();
  }
}

function closeSearch() {
  if(searchVisible) {
    toggleSearch();
  }
}

Add event handlers for search button click, search result click and document click

document.getElementById("SearchButton").addEventListener("click", function(e){
  e.stopPropagation();
  openSearch();
});

document.getElementById("fastSearch").addEventListener("click", function(e){
  e.stopPropagation();
});

document.addEventListener("click", function(){
    closeSearch();
});

 

Change search data

Sending and searching full text content is slow especially when there are a lot of posts. Remove full content and add summary and table of contents to index.json

Add series with tag series:

"series" .Params.series 

Remove html tags, newline characters and extra spaces from table of contents and add as toc:

"toc" (.TableOfContents | replaceRE `<(?:"[^"]*"['"]*|'[^']*'['"]*|[^'">])+>|\\n` "" | replaceRE `\s+`  " ")

Format date for showing with search results and add as data:

"date" (dateFormat "Jan 2, 2006" .Date)

Full index.json

{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
    {{- $.Scratch.Add "index" (dict
    "date" (dateFormat "Jan 2, 2006" .Date) 
    "title" .Title 
    "summary" .Summary 
    "tags" .Params.tags 
    "categories" .Params.categories 
    "series" .Params.series 
    "toc" (.TableOfContents | replaceRE `<(?:"[^"]*"['"]*|'[^']*'['"]*|[^'">])+>|\\n` "" | replaceRE `\s+`  " ") 
    "permalink" .Permalink) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

Change searchable keys in options passed to fuse.js
In function loadSearch in fastsearch.js:

keys: [
  'title',
  'permalink',
  'summary',
  'categories'
  'tags',
  'toc'
]

 

Exact match

hugofastsearch script uses location, distance and threshhold options for fuse.js that enable fuzzy match for the search term. It is approximate match and only searches up to distance * threshold characters from location. Replace it with complete term match (threshold 0) and no limit on distance (ignoreLocation). In function loadSearch in fastsearch.js

var options = {
  ignoreLocation: true,
  threshold: 0,
}