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.
- 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. - Any surrounding page content, such as a heading above the results, is edited on the page in Elementor as normal.
- 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:
| Block | What it shows |
|---|---|
| Search box | The term that was searched. Typing in it re-runs the search straight away. |
| Filters | An All button followed by a list of categories. |
| Results count | For example, 10 of 42 results for "interest free". |
| Results | The list of matching pages. |
| Pagination | Page 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 ID | What it's for |
|---|---|
search-container and search-layout | Page wrappers |
#searchbox, #refinement-list, #results-count, #hits, #pagination | The containers each block is drawn into |
Filters
| Class or ID | What it's for |
|---|---|
#categories-all | The All button |
#categories-filter | The category list |
ck-search-filter | A filter button or link |
ck-search-filter-label and ck-search-filter-count | The label and the count inside a filter |
is-active | Applied to the All button while no category is selected |
Results
| Class | What it's for |
|---|---|
ck-search-result | A single result link |
ck-search-result-badges and ck-search-result-badge | The badge row and a single badge |
highlight | Added to the Q Card and Q Mastercard badges |
ck-search-result-title and ck-search-result-text | The 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>