Q Card Docs
Modules

Search results

The global search results page, powered by Algolia and InstantSearch.js.

Search results

Description

The search results page is where customers land after using the search in the header. Unlike the rest of the site, the results are not built by WordPress — they come from Algolia, a hosted search service, and are drawn onto the page by a library called InstantSearch.js. That is why the results, filters, and counts all update immediately as the filter changes, without the page reloading.

How to edit

There is no CK template for this page, and the results are produced by code rather than by widgets, so there is little to edit in the page builder.

  1. The search term comes from the header search. Searching adds the term to the address as ?s=your term, and the page reads it from there and fills in the search box.
  2. Any surrounding page content, such as a heading above the results, is edited on the page in Elementor as normal.
  3. The results, the filters, the counts, the "no results" wording, and the number of results per page are all set in the script, so changing any of those is a developer task.

What is on the page

The page is made up of five blocks, in this order:

BlockWhat it shows
Search boxThe term that was searched. Typing in it re-runs the search straight away.
FiltersAn All button followed by a list of categories.
Results countFor example, 10 of 42 results for "interest free".
ResultsThe list of matching pages.
PaginationPage numbers below the results.

Filters

The filters come from the Help & Support categories, so any category used on a Help & Support post can appear here. Up to ten are listed, sorted alphabetically, and there is no "show more" link, so an eleventh category would not appear.

Two categories are given friendlier labels on this page: QC shows as Q Card and QMC shows as Q Mastercard. Every other category is shown exactly as it is named in WordPress.

Each filter has a count beside it. The category counts are for the current search, and the count on All is deliberately different — it comes from a separate search that ignores whichever category is selected, so it always shows the total for the search term. Selecting All clears the category filter rather than applying one.

When there are no categories to show, the filter list hides itself rather than leaving an empty space.

Results

Each result is a link made up of:

  • Badges for its categories. Q Card and Q Mastercard come first and are highlighted, then any other categories follow.
  • The page title.
  • A short piece of text, taken from the page's excerpt where it has one.

Ten results are shown per page. When nothing matches, the count reads "No results found" with the search term, and the results area reads "No results found for your query."

Where a result's stored link is a full web address on the live domain, it is shortened to a site-relative link. That keeps results working when the site is viewed on another domain, such as a staging site.

Where the results come from

Results are served from an Algolia search index of the site's content — the wp_searchable_posts index — rather than from WordPress directly. The connection details come from the Algolia plugin in WordPress, using a search-only key.

This means the index has to stay in step with the site. If new or updated content is missing from search while it appears correctly everywhere else, the index most likely needs re-syncing. Raise that with a developer rather than editing this page.

Classes

Primary IDs and classes used by the search results page.

Layout

Class or IDWhat it's for
search-container and search-layoutPage wrappers
#searchbox, #refinement-list, #results-count, #hits, #paginationThe containers each block is drawn into

Filters

Class or IDWhat it's for
#categories-allThe All button
#categories-filterThe category list
ck-search-filterA filter button or link
ck-search-filter-label and ck-search-filter-countThe label and the count inside a filter
is-activeApplied to the All button while no category is selected

Results

ClassWhat it's for
ck-search-resultA single result link
ck-search-result-badges and ck-search-result-badgeThe badge row and a single badge
highlightAdded to the Q Card and Q Mastercard badges
ck-search-result-title and ck-search-result-textThe title and excerpt inside a result

Styling is done through a combination of the built-in page builder options as well as custom CSS where needed.

Script

This is added using the HTML editor template on the search results page. It loads the Algolia and InstantSearch libraries from a content delivery network, then builds the search box, filters, count, results, and pagination.

The library versions are pinned and checked against a security hash, so updating them is a developer task rather than a matter of changing the address.

<script
    src="https://cdn.jsdelivr.net/npm/algoliasearch@5.56.0/dist/lite/builds/browser.umd.js"
    integrity="sha256-FJAyZG3HT2S1oTZ1r2kwy0SBe/MC5MWzteI2GpIsKG0="
    crossorigin="anonymous"
></script>

<script
    src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.108.0/dist/instantsearch.production.min.js"
    integrity="sha256-Ao5EWxyAiSNIbKFYWiz193sFGCSxEQE9aAwH0eKcrFY="
    crossorigin="anonymous"
></script>

<div class="search-container">
    <div class="search-layout">
        <div id="searchbox"></div>

        <div id="refinement-list">
            <div id="categories-all"></div>

            <div id="categories-filter"></div>
        </div>

        <div id="results-count"></div>

        <div id="hits"></div>
    </div>

    <div id="pagination"></div>
</div>

<script>
    const generateBadges = (taxonomies, html) => {
        const cats = new Set(taxonomies.hs_cats || []);

        const badges = [];

        if (cats.has("QC")) {
            badges.push(html`
                <div class="ck-search-result-badge highlight">Q Card</div>
            `);
        }

        if (cats.has("QMC")) {
            badges.push(html`
                <div class="ck-search-result-badge highlight">Q Mastercard</div>
            `);
        }

        (taxonomies.hs_cats || [])
            .filter((cat) => cat !== "QC" && cat !== "QMC")
            .forEach((cat) => {
                badges.push(html`
                    <div class="ck-search-result-badge">${cat}</div>
                `);
            });

        return badges;
    };

    document.addEventListener("DOMContentLoaded", () => {
        // -----------------------------------------
        // Algolia client
        // -----------------------------------------

        const searchClient = window.algoliasearch(
            algolia.application_id,
            algolia.search_api_key,
        );

        // -----------------------------------------
        // Search query
        // -----------------------------------------

        const urlParams = new URLSearchParams(window.location.search);

        const searchQuery = urlParams.get("s") || "";

        // -----------------------------------------
        // InstantSearch
        // -----------------------------------------

        const search = instantsearch({
            indexName: "wp_searchable_posts",

            searchClient,

            initialUiState: {
                wp_searchable_posts: {
                    query: searchQuery,
                },
            },
        });

        // -----------------------------------------
        // Custom category labels
        // -----------------------------------------

        const filtersLabelMap = {
            QC: "Q Card",

            QMC: "Q Mastercard",
        };

        // -----------------------------------------
        // ALL FILTER
        //
        // Connect directly to the same menu attribute.
        // This means refine() can remove the currently
        // selected category.
        // -----------------------------------------

        const renderAllFilter = (renderOptions) => {
            const { items, refine } = renderOptions;

            const container = document.querySelector("#categories-all");

            if (!container) {
                return;
            }

            // Find the currently selected category
            const selectedItem = items.find((item) => item.isRefined);

            const isActive = !selectedItem;

            container.innerHTML = `

            <button
                type="button"
                class="ck-search-filter ${isActive ? "is-active" : ""}"
                id="all-categories-button"
            >

                <span class="ck-search-filter-label">
                    All
                </span>

                <span
                    class="ck-search-filter-count"
                    id="all-categories-count"
                >
                    0
                </span>

            </button>

        `;

            const button = container.querySelector("#all-categories-button");

            if (!button) {
                return;
            }

            // Remove the selected category
            button.addEventListener("click", () => {
                if (selectedItem) {
                    refine(selectedItem.value);
                }
            });
        };

        const customAllFilter =
            instantsearch.connectors.connectMenu(renderAllFilter);

        // -----------------------------------------
        // WIDGETS
        // -----------------------------------------

        search.addWidgets([
            // -------------------------------------
            // Results per page
            // -------------------------------------

            instantsearch.widgets.configure({
                hitsPerPage: 10,
            }),

            // -------------------------------------
            // Search box
            // -------------------------------------

            instantsearch.widgets.searchBox({
                container: "#searchbox",

                placeholder: "",
            }),

            // -------------------------------------
            // All filter
            // -------------------------------------

            customAllFilter({
                container: "#categories-all",

                attribute: "taxonomies.hs_cats",
            }),

            // -------------------------------------
            // Category menu
            // -------------------------------------

            instantsearch.widgets.menu({
                container: "#categories-filter",

                attribute: "taxonomies.hs_cats",

                limit: 10,

                showMore: false,

                sortBy: ["name:asc"],

                templates: {
                    item(item, { html }) {
                        const label = filtersLabelMap[item.value] || item.value;

                        return html`
                            <a
                                class="ck-search-filter"
                                href="${item.url}"
                                data-filter="${item.value}"
                            >
                                <span class="ck-search-filter-label">
                                    ${label}
                                </span>

                                <span class="ck-search-filter-count">
                                    ${item.count}
                                </span>
                            </a>
                        `;
                    },
                },
            }),

            // -----------------------------------------
            // RESULTS COUNT
            // -----------------------------------------

            instantsearch.connectors.connectStats((renderOptions) => {
                const { nbHits, hitsPerPage, page, query } = renderOptions;

                const container = document.querySelector("#results-count");

                if (!container) {
                    return;
                }

                // ---------------------------------
                // Current filtered results
                // ---------------------------------

                if (nbHits === 0) {
                    container.innerHTML = `No results found${
                        query ? ` for "${query}"` : ""
                    }`;

                    return;
                }

                const firstResult = page * hitsPerPage + 1;

                const lastResult = Math.min((page + 1) * hitsPerPage, nbHits);

                const displayedResults = lastResult - firstResult + 1;

                container.innerHTML = `

                    ${displayedResults}
                    of
                    ${nbHits}
                    results${query ? ` for "${query}"` : ""}

                `;
            })({
                container: "#results-count",
            }),

            // -----------------------------------------
            // SEARCH RESULTS
            // -----------------------------------------

            instantsearch.widgets.hits({
                container: "#hits",

                templates: {
                    item(hit, { html }) {
                        let permalink = hit.permalink;

                        if (permalink.includes("qc.humm-group.com")) {
                            permalink = permalink.split(".com")[1];
                        }

                        return html`
                            <a class="ck-search-result" href="${permalink}">
                                ${hit.taxonomies &&
                                hit.taxonomies.hs_cats &&
                                hit.taxonomies.hs_cats.length > 0
                                    ? html`
                                          <div class="ck-search-result-badges">
                                              ${generateBadges(
                                                  hit.taxonomies,
                                                  html,
                                              )}
                                          </div>
                                      `
                                    : ""}

                                <h6 class="ck-search-result-title">
                                    ${hit.post_title || hit.title}
                                </h6>

                                <p class="ck-search-result-text">
                                    ${hit.post_excerpt || hit.content}
                                </p>
                            </a>
                        `;
                    },

                    empty: "No results found for your query.",
                },
            }),

            // -----------------------------------------
            // PAGINATION
            // -----------------------------------------
            instantsearch.widgets.pagination({
                container: "#pagination",
                padding: 2,
            }),
        ]);

        // -----------------------------------------
        // ALL COUNT
        //
        // Run a separate search using only the
        // current search query and NO category
        // refinement.
        // -----------------------------------------

        const updateAllCount = async () => {
            const countElement = document.querySelector(
                "#all-categories-count",
            );

            if (!countElement) {
                return;
            }

            try {
                const response = await searchClient.search([
                    {
                        indexName: "wp_searchable_posts",

                        params: {
                            query: searchQuery,

                            hitsPerPage: 0,
                        },
                    },
                ]);

                const totalCount = response.results[0].nbHits;

                countElement.textContent = totalCount;
            } catch (error) {
                console.error("Unable to retrieve All category count:", error);
            }
        };

        // -----------------------------------------
        // Update the All count whenever the search
        // query changes.
        // -----------------------------------------
        search.on("render", () => {
            updateAllCount();
        });

        // -----------------------------------------
        // Start search
        // -----------------------------------------
        search.start();
    });
</script>

<style>
    #categories-filter:has(> div:empty) {
        display: none;
    }
</style>

On this page