Live Search With HUGO

HUGO is static, that’s a fact. How can I implement a live search? Searching the internet provided me only solutions that require a page refresh, this time of age performance is key, so that’s why I wanted a fast and fuzzy search implementation.

Research

Some this I found which helped to get there are:

Create a JSON object containing all articles

Actually every data you want to search, in this guide (and on this website) I use the following data:

  1. Title
  2. Date
  3. Author
  4. Tags
  5. Content

This is specified in a custom layout. Note the (dict "title" ...) line. You can add any data that HUGO processes (for each article). Its a list of key/values, the keys are presented between the quotes, the value as first value.

layouts/json/single.html

{{- $.Scratch.Add "index" slice -}}
{{- range where .Site.Pages "Type" "not in"  (slice "page" "json") -}}
{{- $.Scratch.Add "index" (dict "title" .Title "date" .Date "author" .Params.author "href" .Permalink "tags" .Params.tags "content" .Plain) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

Now, with this file in place the next thing to do is to create a content page, where this layout is used. This file triggers the creation of “index.json”.

content/search.md

---
date: "2017-03-05T21:10:52+01:00"
type: "json"
url: "index.json"
---

Example of the data returned You can checkout the json object for this website, just go to https://hagfi.sh/index.json

[
  {
    "author": "Kristof Vandam",
    "content": "HUGO is static, that\u0026rsquo;s a fact. How can I implement a live search? Searching the internet provided me only solutions that require a page refresh, this time of age performance is key, so that\u0026rsquo;s why I wanted a fast and fuzzy search implementation. Research Some this I found which helped to get there are:\n https://gohugo.io/tools/search/ ",
    "date": "2018-08-29T22:44:46+02:00",
    "href": "http://localhost:1313/development/live-search-with-hugo/",
    "tags": null,
    "title": "Live Search With HUGO"
  }
]

Add the required dependencies (we use CDN’s)

Make sure the following dependencies are loaded between the head tags. We use a little trick to let the browser decide if http or https is used. These are called Protocol-Relative URL’s.

<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<script src="//cdn.bootcss.com/fuse.js/3.2.0/fuse.min.js"></script>

Add the actual search logic

It’s a best practice to add the JavaScript right before the closing body tags. I highly suggest checking out VueJS with Webpack, but in this case a some simple JS inside script tags will do just fine.

I will go over each section to clarify.

var app = new Vue({
  el: '#app',
  data: {
    fuse: null,
    search: "",
    result: [],
    index: []
  },
  mounted() {

    let self = this

    let options = {
      shouldSort: true,
      threshold: 0.6,
      location: 0,
      distance: 100,
      maxPatternLength: 32,
      minMatchCharLength: 1,
      keys: [
        "title",
        "author",
        "date",
        "content"
      ]
    }
    axios.get('/index.json')
    .then(function (response) {
      self.index = response.data
      self.fuse = new Fuse(response.data, options);
      self.result = fuse.search("");
    })
    .catch(function (error) {
      console.log(error)
    })
  },
  watch: {
    search(nval, oval) {
      if (nval.length > 0) {
        this.result = this.fuse.search(nval)
      } else {
        this.result = []
      }
    }
  }
})

Create the Vue instance

When creating a new Vue instance we assign Vue to a DOM element, most of the time an ID on your body tag is used.

var app = new Vue({
  el: '#app',
  ...
})

Create a data object

This object is accesible across your DOM and Vue instance. Inside functions you can reffer to these with this.*.language-
I initiated some variables like ‘fuse’ so it can be used inside watch and methods.

data: {
  fuse: null,
  search: "",
  result: [],
  index: []
},

What todo when everything is ready

The mounted() function is triggered when everything ready to start processing your custom code. (This function used to name ‘ready()’).
We assign this to self to handle some scope issues in the axios promise.
We polulate some options for FuseJS, note that the keys array is important here. Here we specify which keys of our index.json we want to search.
The index.json file is loaded with AJAX, this way the page should not wait for content that is not required immediately.
When axios retrieves the date we create a Fuse instance (assigned to self.fuse (or this.fuse)).

mounted() {

  let self = this

  let options = {
    shouldSort: true,
    threshold: 0.6,
    location: 0,
    distance: 100,
    maxPatternLength: 32,
    minMatchCharLength: 1,
    keys: [
      "title",
      "author",
      "date",
      "content"
    ]
  }
  axios.get('/index.json')
  .then(function (response) {
    self.index = response.data
    self.fuse = new Fuse(response.data, options);
    self.result = fuse.search("");
  })
  .catch(function (error) {
    console.log(error)
  })
},

When something is entered inside the search field

We watch for this.search to change, if it changes this function is called. Remember we set search: "" inside our data object? If the ‘nval’ (New VALue) is larger than 0 characters we trigger the search function of fuse, which will return a new data set, but filtered. This dataset is stored inside this.result.language-

If the length of ‘nval’ changes to 0 characters we hardcode the result to be an empty array (to prevent possible edgecases).

watch: {
  search(nval, oval) {
    if (nval.length > 0) {
      this.result = this.fuse.search(nval)
    } else {
      this.result = []
    }
  }
}

Ok, cool, now how do I showcase the results?

Well, it’s up to you. The most important parts in this example are:

  1. Bind this.search to the input field (with v-model)
  2. Loop through this.result with v-for, it will recreate the li tag ‘for each’ result item.
  3. Use the result item, reffered as r.
  4. Links are extracted from the result item by the ‘href’ key and bound to the href attribute. :href="r.href"

We use Moment.js to format the default (can be changed) HUGO date format to ’D’ (Day), ‘MMM’ (Month, max 3 characters), ‘YYYY’ (Full Year).

<div class="search-wrapper">
  <input type="text" placeholder="Search ..." v-model="search" class="search"/>
  <ul class="result-items">
    <li v-for="r of result" class="result-item">
      <div class="result-item-wrapper">
        <div class="result-item-left">
          <span class="post-date">
            <span class="post-date-day"><sup v-text="moment(r.date).format('D')"></sup></span><span class="post-date-separator">/</span><span class="post-date-month" v-text="moment(r.date).format('MMM')"></span> <span class="post-date-year" v-text="moment(r.date).format('YYYY')"></span>
          </span>
            <template v-if="r.author">By <a class="post-author" v-text="r.author"></a></template>
        </div>
        <div class="result-item-left">
          <span class="nav-item-separator">//</span><a :href="r.href" v-text="r.title"></a>
        </div>
      </div>
    </li>
  </ul>
</div>
More Reading
Older// Let's Encrypt
comments powered by Disqus