Instant Jekyll Search

X @urre

Instant Jekyll Search

Adding search functionality to Jekyll (or whatever static site generator you are using) is not hard or complicated. In this walkthrough I will show you how to make an simple but fast and instant search for your website. I will be using just Vanilla JavaScript with some ES6.

I have been using the excellent lunr.js lately, a blazing-fast simple full text search engine for client side applications. It is designed to be small, yet full featured, enabling you to provide a great search experience without the need for external, server side, search services. Think Solr but way simpler.

Also, an important part: lunr.js has no external dependencies.

Checkout a live demo


The search template

First, let’s make a simple search template:

<div class="container">
	<div class="columns">
		<div class="column col-6">
			<form class="searchform" action="/sok" method="get">
				<input type="text" class="form-input searchfield" placeholder="Sök album, musiker, skivbolag, taggar..." autofocus>
				<input type="submit" class="invisible">
			</form>
		</div>
		<div class="column col-6">
			<p class="searchcount text-right"></p>
		</div>
	</div>
</div>

JSON data

Now, we need to create a data store that contains basic information about each post. We can output everything we want to be able to search for, in this case; all the details for records on Jazztips. Using the jsonify filter we can safely convert data to JSON format.

I am doing this on a custom Jekyll layout, that the search template uses.

<script>

window.store = [
	{% for post in site.posts %} {
		"title": {{post.title | jsonify}},
		"artist": {{post.artist | jsonify}},
		"link": {{ post.url | jsonify }},
		"label": {{ post.label | jsonify }},
		"image": {{ post.image | jsonify }},
		"date": {{ post.date | date: '%B %-d, %Y' | jsonify }},
		"excerpt": {{ post.content | strip_html | truncatewords: 20 | jsonify }}
	}
	{% unless forloop.last %}, {% endunless %}
	{% endfor %}
]

</script>

Build the index

First import Lunr and store references to our DOM elements

import lunr from 'lunr'

const searchform = document.querySelector('.searchform')
const searchfield = document.querySelector('.searchfield')
const resultdiv = document.querySelector('.searchcontainer')
const searchcount = document.querySelector('.searchcount')

Next, give Lunr all the data so it can help us find posts. A boost tells Lunr that it should favor this field.

let index = lunr(function() {
  this.ref('id')
  this.field('title', {boost: 10})
  this.field('artist')
  this.field('link')
  this.field('image')
  this.field('content')
  this.field('label')
  this.field('tags')
})

for (let key in window.store) {
  index.add({
    'id': key,
    'title': window.store[key].title,
    'artist': window.store[key].artist,
    'link': window.store[key].link,
    'image': window.store[key].image,
    'content': window.store[key].content,
    'label': window.store[key].label,
    'tags': window.store[key].tags,
  })
}

Ok, so we have the JSON-file containing data about all our posts, and we have created the index for Lunr. What’s next?

Well, we need to query Lunr, match results and present this to the user.

First, we listen for when the user types in the search form.

const getTerm = function() {
  searchfield.addEventListener('keyup', function(event) {
    event.preventDefault()
    const query = this.value
    doSearch(query)
  })
}

Then, we want to do the following:

  1. Do the actual search in the index.
  2. Show the number of results.
  3. Update the browser url with a query parameter and display the results.

1. Do the search and show number of results

const doSearch = query => {
  const result = index.search(query)
  resultdiv.innerHTML = ''
  searchcount.innerHTML = `Found ${result.length} records`
  updateUrlParameter(query)
  showResults(result)

}

2. Show the results

const showResults = (result) => {

    for (let item of result) {
      const ref = item.ref
      const searchitem = document.createElement('div')
      searchitem.className = 'searchitem'
      searchitem.innerHTML = `<div class='card'><a class='card-link' href='${window.store[ref].link}'><div class='card-image'><div class='loading'><img class='b-lazy img-responsive' src='${window.store[ref].image}' data-src='${window.store[ref].image}' alt='${window.store[ref].title}'/></div></div><div class='card-header'><h4 class='card-title'>${window.store[ref].artist} - ${window.store[ref].title}</h4><h6 class='card-meta'>${window.store[ref].label}</h6></div></a></div>`

      resultdiv.appendChild(searchitem)

      setTimeout(() => {
        bLazy.revalidate()
      }, 300)
}

Note: I am using bLazy for lazy loading my images, so I update blazy when results is added to the dom.

3. Update browser url with query parameter

Add the search query to the browser url using HTML5 pushState()

const updateUrlParameter = (value) => {
  window.history.pushState('', '', `?s=${encodeURIComponent(value)}`)
}

Also we want to be able to send a link ex: https://jazztips.se/sok/?s=wayne with a search query, just grab the url query string and do a search.

const getQuery = () => {
  const parser = document.createElement('a')
  parser.href = window.location.href

  if(parser.href.includes('=')) {
    const searchquery = decodeURIComponent(parser.href.substring(parser.href.indexOf('=') + 1))
      searchfield.setAttribute('value', searchquery)

      doSearch(searchquery)
  }

}

Notes

This is a basic example, a bit simplified. You could use a template engine to avoid html in JS when you show the results. Also taking care of the the keyup event smarter using debouncing to make it more performant.

Another great option is to use Algolia, they have great documentation about this on their blog

Checkout a live demo