Bloomreach: Analytics Implementation Guide | OpsBlu Docs

Bloomreach: Analytics Implementation Guide

Technical guide to implementing analytics on Bloomreach Experience Manager, covering Freemarker templates, hst:component architecture, and headless...

Analytics Architecture on Bloomreach

Bloomreach Experience Manager (brXM, formerly Hippo CMS) uses a Java-based CMS with an HST (Hippo Site Toolkit) delivery tier. Pages are composed of hst:component nodes that map to Freemarker (or JSP) templates. The rendering pipeline is component-based: each component independently fetches content from the repository and renders its own HTML fragment.

The request flow:

Request → CDN/Load Balancer → HST Pipeline → Component Rendering (Freemarker) → HTML Assembly → Response

Bloomreach also offers Content SaaS, a headless API for decoupled frontends. In headless mode, the CMS delivers JSON via the Content Delivery API, and analytics implementation shifts entirely to the frontend framework (React, Next.js, etc.).

Bloomreach Engagement (formerly Exponea) is the platform's native analytics and CDP product. If you use Engagement alongside a third-party analytics tool, the data layer must serve both.


Installing Tracking Scripts

Method 1: HST Base Page Component (Freemarker)

The base page layout in brXM is a Freemarker template. Inject analytics scripts in the <head> section:

<#-- /WEB-INF/freemarker/hstdefault/base-layout.ftl -->
<!DOCTYPE html>
<html lang="${hstRequestContext.resolvedMount.mount.locale!'en'}">
<head>
  <meta charset="UTF-8"/>
  <@hst.headContributions categoryIncludes="analytics"/>

  <script>
    window.dataLayer = window.dataLayer || [];
  </script>
  <script async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX"></script>
</head>
<body>
  <@hst.include ref="main"/>

  <noscript>
    <iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXX"
            height="0" width="0" style="display:none;visibility:hidden"></iframe>
  </noscript>
</body>
</html>

Method 2: Head Contributions from Components

Individual HST components can contribute scripts to the <head> using the head contribution mechanism:

<#-- Component-level Freemarker template -->
<@hst.headContribution category="analytics">
  <script>
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'component_render',
      componentType: '${hstComponent.componentConfiguration.canonicalPath!""}'
    });
  </script>
</@hst.headContribution>

The <@hst.headContributions> call in the base layout collects all contributions and renders them in order. Use the category attribute to control which contributions appear where.

Method 3: Channel Manager Configuration

brXM's Channel Manager allows per-channel configuration. Store the tracking ID as a channel parameter:

# In the channel's hst:channel node
hst:channel:
  hst:channelinfo:
    class: com.mysite.beans.AnalyticsChannelInfo
  properties:
    gtmId: GTM-XXXXX
    analyticsEnabled: true
// AnalyticsChannelInfo.java
public interface AnalyticsChannelInfo extends ChannelInfo {
    @Parameter(name = "gtmId", defaultValue = "")
    String getGtmId();

    @Parameter(name = "analyticsEnabled", defaultValue = "true")
    Boolean isAnalyticsEnabled();
}

Access in Freemarker:

<#assign channelInfo = hstRequestContext.resolvedMount.mount.channelInfo>
<#if channelInfo.analyticsEnabled?? && channelInfo.analyticsEnabled>
  <script async src="https://www.googletagmanager.com/gtm.js?id=${channelInfo.gtmId}"></script>
</#if>

Data Layer Implementation

Server-Side Data Layer via HST Component

Create a dedicated data layer component in the HST component tree:

// DataLayerComponent.java
public class DataLayerComponent extends BaseHstComponent {
    @Override
    public void doBeforeRender(HstRequest request, HstResponse response) {
        HippoBean document = getContentBean(request);
        Map<String, Object> dataLayer = new LinkedHashMap<>();

        dataLayer.put("platform", "bloomreach");
        dataLayer.put("channel", request.getRequestContext()
            .getResolvedMount().getMount().getName());

        if (document != null) {
            dataLayer.put("contentId", document.getCanonicalUUID());
            dataLayer.put("contentType", document.getNode().getPrimaryNodeType().getName());
            dataLayer.put("contentPath", document.getPath());

            if (document instanceof HippoDocument) {
                Calendar lastMod = ((HippoDocument) document).getLastModified();
                if (lastMod != null) {
                    dataLayer.put("lastModified", lastMod.toInstant().toString());
                }
            }
        }

        request.setAttribute("analyticsDataLayer", dataLayer);
    }
}

Render as JSON in the Freemarker template:

<#-- datalayer.ftl -->
<@hst.headContribution category="analytics">
  <script>
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push(${objectMapper.writeValueAsString(analyticsDataLayer)});
  </script>
</@hst.headContribution>

Headless Mode (Content SaaS API)

In a decoupled architecture using the Content Delivery API, build the data layer on the frontend:

// React/Next.js example
import { initialize, Page } from '@bloomreach/spa-sdk';

function AnalyticsProvider({ configuration, page }) {
  useEffect(() => {
    if (!page) return;

    const document = page.getDocument();
    const dataLayer = {
      platform: 'bloomreach',
      pageTitle: document?.getData()?.title || '',
      contentType: document?.getData()?.contentType || '',
      channel: configuration.channelId,
    };

    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push(dataLayer);
  }, [page]);

  return null;
}

E-commerce Tracking

Bloomreach Discovery provides product search and recommendations. Track interactions between Discovery and your analytics:

// Track Bloomreach Discovery search events
document.addEventListener('br:search:results', function(e) {
  window.dataLayer.push({
    event: 'search_results',
    searchQuery: e.detail.query,
    resultCount: e.detail.totalResults,
    searchEngine: 'bloomreach_discovery'
  });
});

// Track product click from Discovery recommendations
document.addEventListener('br:reco:click', function(e) {
  window.dataLayer.push({
    event: 'product_click',
    productId: e.detail.pid,
    productName: e.detail.title,
    source: 'bloomreach_recommendation',
    widgetId: e.detail.widgetId
  });
});

If using Bloomreach Engagement (Exponea), its SDK tracks events natively. Bridge to your third-party analytics:

// Bridge Bloomreach Engagement events to GTM dataLayer
exponea.on('track', function(eventName, eventData) {
  window.dataLayer.push({
    event: 'br_engagement_' + eventName,
    ...eventData
  });
});

Common Issues

Head Contributions Rendering in Wrong Order

If your data layer script appears after the GTM container script, the data layer is empty when GTM initializes. Control ordering with the keyHint attribute:

<@hst.headContribution category="analytics" keyHint="001-datalayer">
  <script>window.dataLayer = [${dataLayerJson}];</script>
</@hst.headContribution>

<@hst.headContribution category="analytics" keyHint="002-gtm">
  <script async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX"></script>
</@hst.headContribution>

Head contributions are sorted lexicographically by keyHint.

Preview Mode Polluting Analytics

brXM's Channel Manager preview renders pages in an iframe with additional CMS toolbars. Analytics scripts fire in preview mode, sending false data.

Detect and suppress:

<#if !hstRequestContext.preview>
  <script async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX"></script>
</#if>

Content SaaS API Not Returning Expected Fields

When using the headless Content Delivery API, not all document fields are included by default. Ensure your document type's content model exposes the fields you need for the data layer. Check the API response at:

GET /delivery/site/v1/channels/{channel}/documents/{path}

Missing fields usually mean the field is not marked as "searchable" or "indexable" in the document type definition.

HST Component Caching Stale Data Layer

brXM's component-level caching can cache a component's rendered output, including data layer values. For the data layer component, disable caching:

<!-- hst:componentconfiguration -->
<sv:property sv:name="hst:servesstatemapping" sv:type="String">
  <sv:value>no-cache</sv:value>
</sv:property>

Platform-Specific Considerations

HST component tree -- The page layout in brXM is defined by the HST configuration tree (hst:sitemap -> hst:pages -> hst:components). Each component node maps to a Java class and Freemarker template. Place analytics-related components at the top level of the page hierarchy so they execute regardless of which content components are present.

Multi-channel deployment -- brXM supports multiple channels (e.g., desktop site, mobile site, SPA) from a single repository. Each channel can have its own tracking configuration via Channel Manager properties. Ensure channel-specific GTM containers or property IDs are set per channel, not globally.

Bloomreach Engagement vs. third-party analytics -- Bloomreach Engagement (the CDP/analytics layer) collects data independently via its own JavaScript SDK. If you also run GA4 or another platform, you have two collection mechanisms on the page. Deduplicate by choosing one as the source of truth for event definitions and bridging events to the other, rather than implementing the same events twice with different APIs.

Experience Manager vs. Content SaaS -- brXM can run as a traditional server-rendered CMS or as a headless API backend. In headless mode, all analytics implementation moves to the frontend SPA. The @bloomreach/spa-sdk provides a React/Vue/Angular integration layer, but it does not handle analytics -- that is your responsibility in the frontend application code.