From 6785a713eba418f84177ef3bb0357459fb58ca88 Mon Sep 17 00:00:00 2001 From: JakeSCahill Date: Tue, 7 Jul 2026 16:42:35 +0100 Subject: [PATCH] Fix missing click analytics on /search/ results page The standalone /search/ InstantSearch page reported searches to Algolia but never sent click/conversion events, so Click & Conversion analytics showed little/no click data (CTR/CVR mostly N/A or 0%). Only the autocomplete dropdown (algolia-script.hbs) was instrumented. Changes: - Enable `insights: true` on the InstantSearch instance. It reuses the global `aa` (search-insights) already initialised in algolia-script.hbs and attaches a queryID with clickAnalytics on each query. - Wire `sendEvent('click', hit, 'Hit Clicked')` into the custom hit template links. Custom templates don't get automatic click tracking, so this is required for clicks to be recorded and attributed. - Suppress empty "root" queries from analytics via `analytics: false`. These fire on initial page load and facet-only browsing and were being logged as "" (~33% of searches), dominating the report and skewing average CTR. Results still return; they're just not counted as a searched term. Note: the "{search_term_string}" query seen in analytics is the sitelinks-searchbox SearchAction template in head-structured-data.hbs being crawled literally by bots; it's expected SEO behavior and left as-is. Co-Authored-By: Claude Opus 4.8 --- src/layouts/search.hbs | 51 +++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/src/layouts/search.hbs b/src/layouts/search.hbs index 44a6801d..34206647 100644 --- a/src/layouts/search.hbs +++ b/src/layouts/search.hbs @@ -62,17 +62,25 @@ const searchClient = { ...baseClient, search(requests) { - if (exactMatch) { - requests = requests.map((request) => { - const params = { ...(request.params || {}) }; - if (params.query) { - params.typoTolerance = false; - params.removeWordsIfNoResults = 'none'; - params.optionalWords = []; - } - return { ...request, params }; - }); - } + requests = requests.map((request) => { + const params = { ...(request.params || {}) }; + // Keep empty "root" queries out of Algolia Search analytics. These + // fire on initial page load and during facet-only browsing (no term + // typed). They still return results, but `analytics: false` stops + // them being recorded as a searched term — otherwise they show up as + // "" and dominate the report and skew average CTR. + if (!params.query) { + params.analytics = false; + } + // Exact match: require every term, exact spelling, no optional-word + // fallback. Only meaningful when there's an actual query. + if (exactMatch && params.query) { + params.typoTolerance = false; + params.removeWordsIfNoResults = 'none'; + params.optionalWords = []; + } + return { ...request, params }; + }); return baseClient.search(requests); }, }; @@ -81,6 +89,12 @@ const search = instantsearch({ searchClient, indexName, + // Enable click/conversion analytics. Reuses the global `aa` + // (search-insights) already initialised in algolia-script.hbs, sets + // clickAnalytics on queries, and attaches a queryID so the click + // events we send below are attributed to the search that produced them. + // https://www.algolia.com/doc/api-reference/widgets/insights/js/ + insights: true, routing: { stateMapping: { // Convert UI state to URL parameters @@ -137,18 +151,23 @@ instantsearch.widgets.hits({ container: '.results', templates: { - item: (hit, { html, components }) => { + item: (hit, { html, components, sendEvent }) => { + // Record a click event attributed to the current query (queryID) + // so Algolia can report CTR/CVR for the results page. Uses + // navigator.sendBeacon under the hood, so it survives the + // navigation triggered by the link. + const onHitClick = () => sendEvent('click', hit, 'Hit Clicked'); const isExternal = hit.objectID.startsWith('https'); if (isExternal) { return html`