Instant Search Box on your Jekyll Site

Personally, I like things to be quick. While I know sometimes we have to wait around for good things, I’d rather move forward.

Like with a search box on a site. I want mine to be fast, just like Jekyll. And so I found a good one in a previous theme I had.

And I made it work for me. All without a plugin.

It’s pretty simple. There is a search box that has some javascript that is watching for any input. As in, if there’s even one letter typed into it. Then we have a search.json file that has a list of the posts with their titles, descriptions, tags, and such in it. When a letter is typed into it, the javascript takes a look in the search.json for any matching posts and displays those posts.

Pretty simple.

Setup

There are three ‘parts’ to the setup. Two javascript parts, and then the list of posts.

Search.json

Here’s the search.json that creates a list of posts. Place it in the root of your jekyll directory (not the _site one), next to your index.html.


---
layout: null
---
[
  {% for post in site.posts %}
  {
    "title"    : "{{ post.title | escape }}",
    "category" : "{{ post.category }}",
    "tags"     : "{{ post.tags | join: ', ' }}",
    "url"      : "{{ site.baseurl }}{{ post.url }}",
    "date"     : "{{ post.date }}",
    "desc"     : "{{ post.excerpt | markdownify | strip_html | strip_newlines | escape_once }}"
    } {% unless forloop.last %},{% endunless %}
    {% endfor %}
  ]

And I had to add that part post.excerpt | markdownify | strip_html | strip_newlines | escape_once to make the description work properly.

Javascripts

Then we have the javascript that does the searching. Download a copy here. Rename it as you please and put in with your javascript files. (Note: for the sake of speed, you’ll probably want to add with all your other javascript files in the same file. But we’ll just keep to the basics so you know it works.)

Include it in your theme, preferably in your footer (for speed), like this:

<script src="{{ site.baseurl }}/js/jekyll-search.js" type="text/javascript"></script>

Now you need to add the function that makes it all happen. Add this below the <script…jekyll-search.js call, otherwise you’ll get a SimpleJekyllSearch not found sort of error.


<script type="text/javascript">
  SimpleJekyllSearch.init({
    searchInput: document.getElementById('search-input'),
    resultsContainer: document.getElementById('results-container'),
    dataSource: '{{ site.baseurl }}/search.json',
    searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}<\/a><\/li>',
    noResultsText: 'No results found',
    limit: 10,
    fuzzy: true,
  });
</script>

And if you want to change how many results show up, change limit: 10 to however many you would like to show up.

You can also change how the results show up, but modifying the searchResultTemplate line. For my theme, I wanted it to show two lines, and then format the title to be larger than the description text. so I used:

searchResultTemplate: '<a href="{url}" title="{desc}"><h2 class="archive__item-title" itemprop="headline">{title}<\/h2><p class="archive__item-excerpt" itemprop="description">{desc}<\/p><\/a>',

I have since changed it a few times, but I like this example as it shows that you can use headers, classes, and other formatting methods.

Search box and Results

The next thing to do is add the search box. It’s a simple line:

<input type="text" id="search-input" placeholder="Type to Search... " size=15>

The size=15 is optional, but nice to shorten it. And honestly, you can put it just about anywhere, on the page as the javascript is just looking for the id="search-input" to know where to keep an eye out.

The last thing to add is to tell the page where you want the results to show up. The javascript will look for a ` id=”results-container”` section to put the results. So you can just add this where you would like them to show:

<ul id="results-container"></ul>

The javascript simply replaces this with the results.

Testing it out.

Now that you have it set up, you should be able to load the jekyll server, load the site, type into the box and see the results show up. Some things I ran into were the search.json not loading due to it being not formatted right. And not having the javascripts in the right order.

Going Faster than Light

Now that you got it up and going, here’s some ideas to make it faster.

First, you can try adding the javascripts to your current javascript file (you only have one, so your site is blazing fast… right?). I won’t go into detail here, as each site’s javascript is different and you might have a conflict to deal with. But it’s a great way to speed it up, as it puts all the javascript into a single file that is downloaded by the client and cached, never needing to be downloaded again.

There is a second way, where we put the javascript into the html of the site. This won’t decrease the total size of the site, but it can help with latency, or the time to get all the files. See, getting each file takes time in and of itself, so less files can mean less time to download.

So, very simple, instead of calling a separate file for the javascript, we include it in the footer of our html:


<script type="text/javascript">
	!function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a="function"==typeof require&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}for(var i="function"==typeof require&&require,o=0;o<r.length;o++)s(r[o]);return s}({1:[function(require,module){module.exports=function(){function receivedResponse(xhr){return 200==xhr.status&&4==xhr.readyState}function handleResponse(xhr,callback){xhr.onreadystatechange=function(){if(receivedResponse(xhr))try{callback(null,JSON.parse(xhr.responseText))}catch(err){callback(err,null)}}}var self=this;self.load=function(location,callback){var xhr=window.XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP");xhr.open("GET",location,!0),handleResponse(xhr,callback),xhr.send()}}},{}],2:[function(require,module){function FuzzySearchStrategy(){function createFuzzyRegExpFromString(string){return new RegExp(string.split("").join(".*?"),"gi")}var self=this;self.matches=function(string,crit){return"string"!=typeof string?!1:(string=string.trim(),!!string.match(createFuzzyRegExpFromString(crit)))}}module.exports=new FuzzySearchStrategy},{}],3:[function(require,module){function LiteralSearchStrategy(){function doMatch(string,crit){return string.toLowerCase().indexOf(crit.toLowerCase())>=0}var self=this;self.matches=function(string,crit){return"string"!=typeof string?!1:(string=string.trim(),doMatch(string,crit))}}module.exports=new LiteralSearchStrategy},{}],4:[function(require,module){module.exports=function(){function findMatches(store,crit,strategy){for(var data=store.get(),i=0;i<data.length&&matches.length<limit;i++)findMatchesInObject(data[i],crit,strategy);return matches}function findMatchesInObject(obj,crit,strategy){for(var key in obj)if(strategy.matches(obj[key],crit)){matches.push(obj);break}}function getSearchStrategy(){return fuzzy?fuzzySearchStrategy:literalSearchStrategy}var self=this,matches=[],fuzzy=!1,limit=10,fuzzySearchStrategy=require("./SearchStrategies/fuzzy"),literalSearchStrategy=require("./SearchStrategies/literal");self.setFuzzy=function(_fuzzy){fuzzy=!!_fuzzy},self.setLimit=function(_limit){limit=parseInt(_limit,10)||limit},self.search=function(data,crit){return crit?(matches.length=0,findMatches(data,crit,getSearchStrategy())):[]}}},{"./SearchStrategies/fuzzy":2,"./SearchStrategies/literal":3}],5:[function(require,module){module.exports=function(_store){function isObject(obj){return!!obj&&"[object Object]"==Object.prototype.toString.call(obj)}function isArray(obj){return!!obj&&"[object Array]"==Object.prototype.toString.call(obj)}function addObject(data){return store.push(data),data}function addArray(data){for(var added=[],i=0;i<data.length;i++)isObject(data[i])&&added.push(addObject(data[i]));return added}var self=this,store=[];isArray(_store)&&addArray(_store),self.clear=function(){return store.length=0,store},self.get=function(){return store},self.put=function(data){return isObject(data)?addObject(data):isArray(data)?addArray(data):void 0}}},{}],6:[function(require,module){module.exports=function(){var self=this,templatePattern=/\{(.*?)\}/g;self.setTemplatePattern=function(newTemplatePattern){templatePattern=newTemplatePattern},self.render=function(t,data){return t.replace(templatePattern,function(match,prop){return data[prop]||match})}}},{}],7:[function(require){!function(window){"use strict";function SimpleJekyllSearch(){function initWithJSON(){store.put(opt.dataSource),registerInput()}function initWithURL(url){jsonLoader.load(url,function(err,json){err?throwError("failed to get JSON ("+url+")"):(store.put(json),registerInput())})}function throwError(message){throw new Error("SimpleJekyllSearch --- "+message)}function validateOptions(_opt){for(var i=0;i<requiredOptions.length;i++){var req=requiredOptions[i];_opt[req]||throwError("You must specify a "+req)}}function assignOptions(_opt){for(var option in opt)opt[option]=_opt[option]||opt[option]}function isJSON(json){try{return json instanceof Object&&JSON.parse(JSON.stringify(json))}catch(e){return!1}}function emptyResultsContainer(){opt.resultsContainer.innerHTML=""}function appendToResultsContainer(text){opt.resultsContainer.innerHTML+=text}function registerInput(){opt.searchInput.addEventListener("keyup",function(e){return 0==e.target.value.length?void emptyResultsContainer():void render(searcher.search(store,e.target.value))})}function render(results){if(emptyResultsContainer(),0==results.length)return appendToResultsContainer(opt.noResultsText);for(var i=0;i<results.length;i++)appendToResultsContainer(templater.render(opt.searchResultTemplate,results[i]))}var self=this,requiredOptions=["searchInput","resultsContainer","dataSource"],opt={searchInput:null,resultsContainer:null,dataSource:[],searchResultTemplate:'<li><a href="{url}" title="{desc}">{title}</a></li>',noResultsText:"No results found",limit:10,fuzzy:!1};self.init=function(_opt){validateOptions(_opt),assignOptions(_opt),isJSON(opt.dataSource)?initWithJSON(opt.dataSource):initWithURL(opt.dataSource)}}var Searcher=require("./Searcher"),Templater=require("./Templater"),Store=require("./Store"),JSONLoader=require("./JSONLoader"),searcher=new Searcher,templater=new Templater,store=new Store,jsonLoader=new JSONLoader;window.SimpleJekyllSearch=new SimpleJekyllSearch}(window,document)},{"./JSONLoader":1,"./Searcher":4,"./Store":5,"./Templater":6}]},{},[7]);

  SimpleJekyllSearch.init({
    searchInput: document.getElementById('search-input'),
    resultsContainer: document.getElementById('results-container'),
    dataSource: '{{ site.baseurl }}/search.json',
    searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}<\/a><\/li>',
    noResultsText: 'No results found',
    limit: 10,
    fuzzy: true,
  });

</script>

And with that, it should load quicker, as it only has to grab one file, rather than two.