Jekyll search using JSON, much faster than Google Search.

Check what was updated. - Wed Aug 8 20:36:45 EDT 2018

I remember using and integrating Google Search results in various web projects of mine, including this blog but never did occur to me that using Google Search for Jekyll was pretty painful. It wasn’t the feature Google was providing their free users; it wasn’t the idea behind the implementation but rather the various factors required to implement it correctly. A small mistake in proper configurations could make our end goal turn south.

I did not notice this problem until a week after I wrote a blog post - Importing and exporting files using ReactJS. and noticed that while searching for import or export in my search page, the blog post was not included in the search results. Like I mentioned before, if not configured right, problems and problems everywhere and so I decided to go the plain and simple way.

The simple way I chose to follow was a little too simple and lacks features for now, which I will eventually add as the time goes by but for starters, definitely gets the job done.

First, let me list1 some of Jekyll’s liquid template syntaxes that I used for this task.

Jekyll Syntaxes

  1. site.posts - We will use it initialize and loop through all our posts.
  2. post.title - To access our post’s title.
  3. post.date - Date, the one we specify in the top of our post’s markdown file.
  4. post.categories - One of the post properties that we set.
  5. post.url - This one is generated by Jekyll during build but can be accessed and has a pattern unless specified.
  6. post.tags - Our blog post’s tags, in case we use it, if not, not necessary.

I used these syntaxes to create and populate a JSON file named posts.json with our blog posts data.

---
layout: null
---
{
    "posts": [
        {% for post in site.posts %}
            {
                "title": "{{ post.title }}",
                "date": "{{ post.date }}",
                "category": "{{ post.categories }}",
                "link": "{{ post.url | replace:'index.html','' | absolute_url }}",
                "tags": {{ post.tags  | append: "" | join: ', ' }}
            }{% unless forloop.last %}, {% endunless %}
        {% endfor %}
    ]
}

Our posts.json file does not necessarily need category and tags JSON keys but I thought it would ease the search process if we can search the categories and tags! Also, we could extend this search feature by initializing and accessing various post properties2.

Our next process includes writing a search trigger which I already made a long time ago when Google Search was still being used for this blog, therefore, all I had to do was modify the search function. There are two conditions we need to focus on, to be able to search our title, tags and categories. I achieved this by using the .filter() method.

If we consider data to be the contents from our posts.json, our search/filter should be similar to the code below as I wanted the readers to be able to search my blog posts by their title, tags and categories.

const filteredData = data.posts.filter(post =>
    post.title.toLowerCase().includes(searchKey.toLowerCase()) ||
    post.tags.join().toLowerCase().includes(searchKey.toLowerCase()) ||
    post.category.toLowerCase().includes(searchKey.toLowerCase()));

The remaining task is to format the filteredData in an organized manner, depending on layout and style of your preference. Mine is a bit messy but works well with the framework and libraries I’ve used!

/* Search trigger - using manual button click.
========================================================= */
$(".gcse-trigger").click(function (e) {
    e.preventDefault();
    var searchKey = $('input#toSearch').val();
    $.getJSON("../posts.json", {}, function (data) {
        const filteredData = data.posts.filter(post =>
            post.title.toLowerCase().includes(searchKey.toLowerCase()) ||
            post.tags.join().toLowerCase().includes(searchKey.toLowerCase()) ||
            post.category.toLowerCase().includes(searchKey.toLowerCase()));
            
        $(".search-result-container").empty()
        .append("<h5 class='totalSearchResults'>Found " + 
        filteredData.length + " results for " + 
        searchKey + ".</h5>");
    
        $.each(filteredData, (key, value) => {
            var initialFormatting = '<div class="row result">' +
                '<div class="u-full-width">' +
                '<h5><a target="_blank" href="{0}">{1}</a></h5>' +
                '<p>{2}</p>' +
                '<p>{3}<br/>{4}</p>' +
                '</div></div>';
            $(".search-result-container").append(
                initialFormatting
                    .replace("{0}", value.link)
                    .replace("{1}", value.title)
                    .replace("{2}", value.tags.join(', ').toUpperCase())
                    .replace("{3}", value.category.toUpperCase())
                    .replace("{4}", value.date.toUpperCase())
            );
        });
    });
});

The outcome I decided to settle with, I am pretty satisfied with it. Search Demo


Updated

I wanted to search not only blog post’s title, tags or categories but a bit of an excerpt from the blog post as well and this is what I came up with. The most significant constraint on searching the post content is the performance, having to search big post contents and not only that, the amount of blog posts creates even bigger performance issues. I didn’t quite overcome this problem yet and have to rethink this approach.

.. but, for the meantime, I decided to add a certain amount of words in our posts.json file to quickly search the given searchKey in our excerpt. The posts.json is changed to the following. Those make up a properly formatted JSON entry for our search file.

"title": "{{ post.title }}",
"date": "{{ post.date }}",
"category": "{{ post.categories }}",
"link": "{{ post.url | replace:'index.html','' | absolute_url }}",
"excerpt": "{{ post.content | markdownify | strip_html | truncatewords: 70 | escape }}",
"tags": {{ post.tags | append: "" | join: ', ' }}

Similarly, our search function has a bit of an addition.

/* Search trigger - using manual button click.
========================================================= */

$(".gcse-trigger").click(function(e) {
    e.preventDefault();
    var searchKey = $("input#toSearch").val();
    console.log(searchKey);
    $.getJSON("../posts.json", {}, function(data) {
        const filteredData = data.posts.filter(
            post =>
                post.title.toLowerCase().includes(searchKey.toLowerCase()) ||
                post.tags
                    .join()
                    .toLowerCase()
                    .includes(searchKey.toLowerCase()) ||
                post.category.toLowerCase().includes(searchKey.toLowerCase()) ||
                post.excerpt.indexOf(searchKey) > 0
        );
        console.log(JSON.stringify(filteredData));
        $(".search-result-container")
            .empty()
            .append(
                "<h5 class='totalSearchResults'>Found " +
                    filteredData.length +
                    " results for " +
                    searchKey +
                    ".</h5>"
            );
        $.each(filteredData, (key, value) => {
            var initialFormatting =
                '<div class="row result">' +
                '<div class="u-full-width">' +
                '<h5><i class="icon-file-text2"></i>&nbsp;<a target="_blank" href="{0}">{1}</a></h5>' +
                '<p><i class="icon-price-tags" title="Tags"></i>&nbsp;&nbsp;{2}</p>' +
                "<p>{5}</p>" +
                '<p><i class="icon-tree" title="Categories"></i>&nbsp;&nbsp;{3}<br/>' +
                '<i class="icon-calendar"></i>&nbsp;&nbsp;{4}</p>' +
                "</div></div>";
            $(".search-result-container").append(
                initialFormatting
                    .replace("{0}", value.link)
                    .replace(
                        "{1}",
                        value.title.replace(
                            searchKey,
                            "<span style='background: yellow;'>" +
                                searchKey +
                                "</span>"
                        )
                    )
                    .replace(
                        "{2}",
                        value.tags
                            .join(", ")
                            .toUpperCase()
                            .replace(
                                searchKey.toUpperCase(),
                                "<span style='background: yellow;'>" +
                                    searchKey.toUpperCase() +
                                    "</span>"
                            )
                    )
                    .replace(
                        "{3}",
                        value.category
                            .toUpperCase()
                            .replace(
                                searchKey.toUpperCase(),
                                "<span style='background: yellow;'>" +
                                    searchKey.toUpperCase() +
                                    "</span>"
                            )
                    )
                    .replace("{4}", value.date.toUpperCase())
                    .replace(
                        "{5}",
                        value.excerpt.replace(
                            searchKey,
                            "<span style='background: yellow;'>" +
                                searchKey +
                                "</span>"
                        )
                    )
            );
        });
    });
});

That’s about it; I’m trying to implement searching of words, without caring about its case - upper, lower or capitalized. I shall update the post once again!

Updated Demo

Simplicity

Enjoy & Happy Coding!

Photo by João Silas on Unsplash.

  • Edited a little, didn’t want the full-sized photo.

  1. The reason, I enlisted these syntaxes is because we need it to generate a new file with those contents and form a valid JSON content. 

  2. Post properties refer to the contents between --- on top of our page, You can add more properties, such as featuredImages etc.