Drupal 8 as a Static Site: JavaScript Search Client

Submitted by nigel on Saturday 15th December 2018

We now need to plug the Elasticsearch query that was built in the previous blog into a JavaScript client that has been loaded on the search page as described in another earlier blog. So a few lines of JavaScript will be required here, and it will need to work on my three environments:

  1. Sandbox Drupal site connecting to local Elasticsearch server.
  2. Sandbox static site connecting to local Elasticsearch server.
  3. Production static site connecting to production Elasticsearch server. 

The JavaScript will use the elasticsearch.js API library which can be downloaded from here. We need to make our theme aware of our JavaScript files and we only wanted them loading on the node search page route. Our JavaScript file will be called beezee_elastic.js, which is the name of my theme (beezee8) and elastic obviously.  

Add the JavaScript to the Beezee theme
Firstly we need an entry in our theme's info.yml file under the Libraries heading.
beezee8.info.yml
libraries:
  - 'beezee8/elastic-library'
Next we need to append to the libraries yml file the location of our JavaScript files - the Elastic API library and our own personal script.
beezee8.libraries.yml
elastic-library:
  js:
    js/elasticsearch-js/elasticsearch.min.js: {}
    js/beezee/beezee_elastic.js: {} 
Obviously we then have to copy our JS files into the filesystem at those locations - for now I will have a placeholder empty file for beezee_elastic.js which we will flesh out later.
Load the JavaScript files on the search page only
There are a few ways of doing this, but my favourite is to use a preprocessor function in the {themename}.theme file. We can use hook_preprocess_page for this.
beezee8.theme
<?php
/**
 * @param $variables
 */
function beezee8_preprocess_page(&$variables) {

    
// If we are on the search page, load the JS search client api
    // and our implementation to Elasticsearch
    
if (\Drupal::routeMatch()->getRouteName() == 'search.view_node_search') {
        
$variables['#attached']['library'][] = 'beezee8/elastic-library';
    }
}
?>
The JavaScript search client
Ok so finally we are ready to build out our front end search client. Here we go!
beezee_elastic.js
(function ($, Drupal) {
 
    var elastic_once;
 
    function BeezeeElastic() {
        if (!elastic_once) {
            elastic_once = true;
 
            // Get the params from the url
            var pageURL = window.location.search.substring(1);
            var URLVariables = pageURL.split('&');
            var keys;
            i = 0;
            while (i < URLVariables.length) {
                var ParameterName = URLVariables[i].split('=');
                if (ParameterName[0] == 'keys') {
                    keys = ParameterName[1];
                }
                i++;
            }
 
            // Did we get anything from the URL? If not bail, otherwise do the search.
            // We shouldn't get to here so don't show a message to user.
            if (!keys) {
                console.log('Invalid keys');
                return;
            }
 
            // Add the search key to the h1 selector
            $("h1").append(Drupal.t(" for ") + keys);
 
            // Endpoint defined during instantiation of API Client
            var client = new elasticsearch.Client({
                host: 'http://meedjum.test:9200/' /*,*/
                /* log: 'trace' */
            });
 
            client.search({
                index: 'elasticsearch_index_badzilla_meedjum_staticcontent',
                body: {
                    query: {
                        function_score: {
                            query: {
                                query_string: {
                                    query: keys,
                                    fields: [
                                        "title",
                                        "*body",
                                        "term"
                                    ],
                                    default_operator: "OR"
                                }
                            },
                            functions: [
                                {
                                    linear: {
                                        "created": {
                                            origin: "now",
                                            offset: "365d",
                                            scale: "1460d",
                                            decay: 0.5
                                        }
                                    }
                                }
                            ]
                        }
                    },
                    highlight: {
                        number_of_fragments: 1,
                        pre_tags: ["<strong>"],
                        post_tags: ["</strong>"],
                        fragment_size: 400,
                        no_match_size: 400,
                        phrase_limit: 1,
                        fields: {
                            "*body": {},
                            "title": {},
                            "term": {}
                        }
                    },
                    size: 10
                }
            }, response);
 
            /*
             Process the response from Elastic
             */
            function response(err, resp, status) {
 
                // Are we logged in? If so our jQuery calls will be slightly different due to additional DOM stuff
                // for non anonymous
                // These are our targets for injecting the content on the page. Markup is Bootstrap 3
                var h1_find;
                var h2_find;
                if ($(".user-logged-in").length) {
                    h1_find = "h1 + nav:last-child";
                    h2_find = "h1 + nav + h2:last-child";
                } else {
                    h1_find = "h1";
                    h2_find = "h1 + h2:last-child";
                }
 
                var hits = resp.hits.hits;
 
                if (resp.hits.total == 0) {
                    $(h1_find).after("<h3>" + Drupal.t("Your search yielded no results.") + "</h3>");
                }
                else {
                    $(h1_find).after("<h2>" + Drupal.t("Search results") + "</h2>");
 
                    var inject = "<ol>";
                    $.each(hits, function (key, value) {
 
                        // Url, title and Body
                        var prefix = "<li><h3>";
                        var suffix = "</li>";
                        var title = "<a href=\"" + value._source.url[0] + "\">" + value.highlight.title[0] + "</a></h3>";
 
                        // do we have a book body or a paragraph body?
                        var body;
                        if (typeof value.highlight.blog_body != 'undefined') {
                            body = value.highlight.blog_body[0];
                        } else {
                            body = value.highlight.body;
                        }
                        body = "<p>" + body + "</p>";
 
                        inject += prefix + title + body + suffix;
 
                        // Add the tag
                        if (typeof value.highlight.term != 'undefined') {
                            inject += "<p>" + Drupal.t("Tag:") + " " + value.highlight.term + "</p>";
                        }
 
                        // Author and date
                        var gb_obj = new Date(value._source.created[0] * 1000);
                        var gb_date = ('0' + gb_obj.getDate()).slice(-2) +
                            "/" + ('0' + (gb_obj.getMonth() + 1)).slice(-2) +
                            "/" + gb_obj.getFullYear() +
                            " " + "-" + "&nbsp" +
                            gb_obj.getHours() + ":" +
                            gb_obj.getMinutes();
 
                        inject += "<p>" + value._source.author[0] + " " + "-" + " " + gb_date;
 
                    });
                    inject += "</ol>";
                    $(h2_find).after(inject);
                }
            }
        }
    }
 
 
    Drupal.behaviors.beezee_elastic = {
        attach: function (context, settings) {
            BeezeeElastic();
        }
    };
})(jQuery, Drupal);
Code commentary

I'd like to think the code is self-explanatory, but here are a few words for additional clarification. 

The first block of code is processing the URL params. We are expecting the ?keys={search_phrase}. The search phrase needs to be injected into the Elasticsearch query later. 

The client object is returned from me instantiating the Elasticsearch API. I needed a parameter of the Elasticsearch endpoint which is currently hardcoded. It will work for my two sandbox environments (Drupal and static) but not for production. I will have to come up with a solution to rewrite the endpoint during the deployment process!

The client.search call is our query created earlier, and I have added the search term from the URL. The Elasticsearch API provides two mechanisms for this call - it can return a promise or invoke a callback. I opted for the latter. 

The response function is the callback and it handles the Elastic response. My first task is to set the selectors where I will inject the results - note that the DOM is different whether I am logged into my sandbox Drupal or not - so my injection anchor points are different dependent upon this circumstance. 

I add the title, body text, tag, author and published date to my output, concatenating them together, and then inject them into the content area of the screen around ordered list tags. 

The next result is very similar visually to the default Drupal core search with the addition of my blog tags, although of course it can be styled differently if wanted. 

The results
results

The script works well on both the Drupal and static sites in sandbox. The screenshot above shows a search on the word nginx. At the moment I haven't built in a paginator, but that could come along later.