Jekyll is a static website generator that takes your plain text files and converts them to a beautiful website/blog.
People mostly use markdown files to write content and then use Jekyll to convert these markdown (.md) files to Html pages. Now, as there are no database nor backend application servers, there are limited ways to add a search feature to such a website. To be more specific, we have the following options:
- Google Custom Search
- Algolia Search Widget
- Custom search with the help of Javascript (may use 3rd party js libraries too)
In this blog, we will discuss the 3rd option and if you want to know why I went with this option and not the other two you can another post which describes how I made my blog.
There are two main steps in this option:
First, we need to have a file that will have details of all our posts. This file will basically act as our database. The RSS xml file can be used but the RSS feed file generated by jekyll-feed plugin contains only the recent 10 posts. This number is hardcoded into the plugin and is not configurable. Therefore, we have two options here, first, fork jekyll-feed plugin which I’ve already done, second, we need to explicitly generate a file that would contain metadata about all the posts. As there are no backend servers nor databases, we would ideally do the search on the client-side, i.e, with the help of Javascript. Therefore, it would be prudent to generate a json file which would contain the details of all our posts. We can then use this json file in our javascript code to search. Sounds simple? Cool, let’s see how we can create the json file.
---
layout: null
---
[
{% for post in site.posts %}
{
"title" : "{{ post.title }}",
"url" : "{{ post.url }}",
"date" : "{{ post.date | date: "%b %d, %Y" }}",
"content" : "{{ post.content | strip | strip_html | strip_newlines | replace: '"', '' | slice: 0, 600 }}"
}{% if forloop.last %}{% else %},{% endif %}
{% endfor %}
]
File: site.json
Place the above file in the root directory of your project. This will create a json array with all the metadata which we can use in our search method. You can even see the site.json file of my website.
Second, implement the search method in Javascript. We basically add an event listener and once our required event is triggered we perform the search. This can be a button click
event or a keyup
event. I chose the latter because I wanted to refresh the results as the user types, just like how Google Search works.
var posts = []; // will hold the json array from your site.json file
$("body").on("click", "[data-action]", function (e) {
e.preventDefault();
var $this = $(this);
var action = $this.data('action');
var target = $this.data('target');
if (action === 'omnisearch-open') {
var $searchFormEl = $(target).find('.form-control');
search($searchFormEl.val()); // to handle cases where the search field isn't empty, i.e, the user searched for something earlier
$searchFormEl.keyup(function (e) { // refresh results while user is typing
e.preventDefault();
search($searchFormEl.val());
});
}
});
function search(searchStr) {
fetchSiteJson(searchCallback(searchStr));
}
function searchCallback(searchStr) {
return function () {
var options = { // initialize options for fuse.js
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
{
name: "title",
weight: 0.3 // give title more importance
},
{
name: "content",
weight: 0.4
}
]
};
// initialize fuse.js library
var fuse = new Fuse(posts, options);
var results = fuse.search(searchStr); // invoke search method in fuse.js library
if (searchStr.length === 0) {
updateResults(posts.slice(0, 5), true); // if there are no search results, show some suggestions
} else {
updateResults(results, false);
}
}
}
In the above code, if you see I’ve used fuse.js library to perform the search. You can simply do a string match in javascript and filter the results but what’s the fun in that, right. Therefore, I compared various client-side search libraries and even used lunr.js at first but later learned that it lacked the Edit distance algorithm which I think is important to be in a search library. The Edit distance algorithm basically takes care of typos and shows the closest results. So, I went with fuse.js and it wasn’t only because of that but fuse.js also has great documentation, is easier to use, does the job well, and last but not least, is the most trending on the market now.
It is simply two lines of code to use the fuse.js library. You initialize the library new Fuse(posts, options);
and then invoke the search method fuse.search(searchStr);
.
Below is the code to update the search results on the UI. This code snippet is taken from this website. You can see how it looks by hitting the search button at the top. Now, this will be different based on your design but I’ve shared it here so that you at least get an idea of what’s involved.
function updateResults(results, isSuggestion) {
var resultsHtml = '';
results.forEach(function (res) {
resultsHtml += '<li>' +
'<a class="list-link" href="' + res.url + '">' +
'<i class="search-icon" data-feather="' + (isSuggestion ? 'clock' : 'search') + '"></i> ' +
'<span>' + res.title + '</span><small> on ' + res.date + '</small>' +
'</a>' +
'</li>';
});
var resultsWrapperHtml = '<h6 class="heading">Search ' + (isSuggestion ? 'Suggestions' : 'Results') + '</h6>' +
'<div class="row">' +
'<div class="col-sm-12">' +
'<ul class="list-unstyled mb-0">' + resultsHtml + '</ul></div></div>';
// insert the result html into the page
$('.omnisearch-suggestions').html(resultsWrapperHtml);
// render feather icons
feather.replace({class: 'search-icon', width: '1em', height: '1em'});
}
Lastly, below is the code to load the site.json
file and get the metadata into an array. If you’re wondering what are those $
signs everywhere are then please read about jQuery framework. This is the only dependency in my code which you can easily get rid of as I am not doing anything complex here.
function fetchSiteJson(callback) {
if (posts.length === 0) {
// fetch site.json file
$.get("/site.json", function (data) {
posts = data;
callback();
});
} else { // we already have the posts so simply use it instead of downloading the file again
callback();
}
}
The above code snippets are taken from this website. If you want to see a live demo of how everything looks and feels like then hit the search button at the top. For any queries, please feel free to drop a comment below. Cheers!