Insights

How To Highlight Search Results In Sitecore SXA

Goal

Write a script that highlights users' search key in Sitecore SXA's OOTB search components.

Pre-Requisite

  • Basic understanding of OOTB (Out Of The Box) search components, specifically the search results and search box component.
  • Understanding of SXA Search's Search Signature (In short, it's a string that you define in SXA in order to restrict interaction between search components. For example, we can configure a search box to only target one of the search components, but not the others.)
  • Cash JS or JQuery

Overview

As of SXA version 10.0.0, there is no out-of-the-box way of highlighting search keys in the search results. So we must write the logic ourselves. This almost seems like a trivial exercise where we wrap strong tag around the search key in paragraphs. However, we need some additional steps to ensure we don't break SXA's OOTB components

Code Structure

The skeleton of the code will look like this. XA.component.search.xxx refers to the js modules that ship with SXA. We MUST use this because we need to listen to the events that SXA search components fire. In this case, we need to listen to 'results-loaded' event and handle the highlighting of search key in onResultsLoaded method that I will define in a bit. This structure is being used by pretty much every single js module that ship with SXA, so please take a look at them if you re confused.

        
        import $ from "cash-dom";

        XA.component.search.resultsUtil = (function ($, document) {
            var api = {};

            [OMITTED CODE]

            // Listen to Events
            XA.component.search.vent.on("results-loaded", onResultsLoaded);
            
            api.init = function () {};
            return api;

        }(jQuery, document));

        XA.register('searchResultsUtil', XA.component.search.resultsUtil);
        
    

Rest Of The Code

Don't be alarmed by the length. This code is identical to the above except that we just filled in the OMITTED CODE with a bunch of functions. Here is what's basically going on:

  • Locating all search keys from the url hash (q= or [signature-name]_q=). For example, if signature name is abcd, we look for abcd_q. if no signature, look for q
  • Locating all search results
  • Match the search signatures between the search results components and search keys
  • In each search reuslts component, if search key exists, locate them in the html and wrap the target strings in bold (ensure case agonostic)
  • Everytime highlightQueryInSearchResults is called again, clean all the strong tags first before highlighting the new search keys.
Now that you know the general flow, the code itself should read pretty well. Begin reading from XA.component.search.vent.on("results-loaded", onResultsLoaded). I have also added more comments than what's on our production code.

 

        
            import $ from "cash-dom";

            XA.component.search.resultsUtil = (function ($, document) {
                var api = {};

                function getQueryMap(url) {
                    // returns a map from query key to value in sxa's context. if no custom signature, we just need to look for q=
                    // with custom signature, need to look for _q=
                    // example 1 (with custom signature): #sig1_e=0&sig1_q=summary -? {sig1_q: 'summary'}
                    // example 2 (no custom signature): #q=summary -> {q: 'summary'}
                    if (!url.includes('#')) return; // sxa search uses hash. no hash, no param

                    const hash = url.split('#')[1];
                    if (hash == '') return;

                    const validQueryList = hash.split('&').filter(hsh => {
                        return hsh.includes('q=');
                    });

                    const queryMap = {};
                    validQueryList.forEach(queryString => {
                        const queryStringSplit = queryString.split('=');
                        if (queryStringSplit[1] != null && queryStringSplit[1] !== '') {
                            queryMap[queryStringSplit[0]] = queryStringSplit[1].replaceAll('%20', ' ');
                        }
                    });

                    return queryMap;
                }

                function findAllWordsInString(str, word) {
                    // returns locations (indices) of the word in str
                    const strLower = str.toLowerCase();
                    const wordLower = word.toLowerCase();
                    const indices = [];
                    let index = 0;
                    let limiter = 0; // prevent infinite loop
                    while (index < strlower.length="" &&="" index="" !="=" -1="" &&="" limiter="" />< 10000)="" {="" index="strLower.indexOf(wordLower," index);="" if="" (index="" !="=" -1)="" {="" prevents="" infinite="" loop="" caused="" by="" indexof="" returning="" -1,="" therefore="" making="" index="" 0="" indices.push(index)="" index="index" +="" 1;="" }="" limiter++;="" }="" return="" indices;="" }="" function="" boldifystringinhtml(str,="" target)="" {="" const="" indices="findAllWordsInString(str," target)="" for="" (let="" i="indices.length" -="" 1;="" i="" /> -1; i--) {
                        const currentStr = str.substring(0, indices[i])
                            + ''
                            + str.substring(indices[i], indices[i] + target.length)
                            + ''
                            + str.substring(indices[i] + target.length);
                        str = currentStr;
                    }
                    return str;
                }

                function cleanStrongElementsInHTML(str) {
                    // removes all strong tags in a string
                    return str.split('').join('').split('').join('');
                }

                function highlightQueryInSearchResults() {
                    const queryMap = getQueryMap(window.location.href);
                    if (queryMap == null) return;
                    const searchResultsList = $('.component.search-results');
                    for (let i = 0; i < searchresultslist.length;="" i++)="" {="" for="" each="" search="" results="" component="" let="" signature="JSON.parse($(searchResultsList[i]).attr('data-properties')).sig;" signature="signature" signature="" :="" '';="" const="" searchresultbody="$(searchResultsList[i]).find('.search-result" .search-result__body');="" for="" (let="" j="0;" j="" />< searchresultbody.length;="" j++)="" {="" const="" searchresultbodyhtml="$(searchResultBody[j]).html();" if="" (searchresultbodyhtml="==" ''="" ||="" searchresultbodyhtml="=" null)="" continue;="" const="" cleansearchresultbodyhtml="cleanStrongElementsInHTML(searchResultBodyHTML);" if="" (signature="==" '')="" {="" no="" custom="" signature="" if="" ('q'="" in="" querymap)="" {="" $(searchresultbody[j]).html(boldifystringinhtml(cleansearchresultbodyhtml,="" querymap['q']));="" }="" }="" else="" {="" if="" (signature="" +="" '_q'="" in="" querymap)="" {="" with="" custom="" signature,="" need="" to="" identify="" />_q
                                    $(searchResultBody[j]).html(BoldifyStringInHTML(cleanSearchResultBodyHTML, queryMap[signature + '_q']));
                                }
                            }
                        }
                    }
                }

                function onResultsLoaded() {
                    setTimeout(() => { //wait for the DOM to load
                        highlightQueryInSearchResults();
                    })
                }

                // Listen to Events
                XA.component.search.vent.on("results-loaded", onResultsLoaded);
                
                api.init = function () {};
                return api;

            }(jQuery, document));

            XA.register('searchResultsUtil', XA.component.search.resultsUtil);

        
    

Final Results

Highlighted keywords in Sitecore search results

Room For Improvement?

One word: REGEX. It would've simplified the process of locating search key in html. At the time of writing, I was not super comfortable with it.

Thank You

👋 Hey Sitecore Enthusiasts!

Sign up to our bi-weekly newsletter for a bite-sized curation of valuable insight from the Sitecore community.

What’s in it for you?

  • Stay up-to-date with the latest Sitecore news
  • New to Sitecore? Learn tips and tricks to help you navigate this powerful tool
  • Sitecore pro? Expand your skill set and discover troubleshooting tips
  • Browse open careers and opportunities
  • Get a chance to be featured in upcoming editions
  • Learn our secret handshake
  • And more!
Sitecore Snack a newsletter by Fishtank Consulting