Analytics Architecture on AEM
Adobe Experience Manager uses a Java-based content repository (JCR) with a component-driven rendering model. Analytics scripts are deployed through the clientlib (client library) system, which manages JavaScript and CSS dependencies across components and templates.
The request lifecycle that affects tracking:
Request → CDN → Dispatcher (Apache/IIS cache) → AEM Publish → Sling Resolution → HTL Rendering → HTML
The Dispatcher cache sits between the client and AEM Publish instances. Like any reverse proxy cache, it serves static HTML to anonymous users. Analytics scripts embedded in the HTML still execute client-side, but any server-side personalization or data layer logic only runs on cache misses unless you use SDI (Sling Dynamic Include) or client-side fetches.
AEM Cloud Service manages Dispatcher configuration through the Cloud Manager pipeline. On-premise AEM (6.5) uses manually configured Apache/IIS Dispatcher modules.
Installing Tracking Scripts
Method 1: Client Library (clientlib)
Create a clientlib category that loads on every page:
/apps/mysite/clientlibs/analytics/
├── .content.xml
├── js.txt
└── analytics.js
<!-- .content.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
categories="[mysite.analytics]"
dependencies="[mysite.base]"/>
// analytics.js
window.dataLayer = window.dataLayer || [];
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXX');
Include the clientlib in your page template's HTL:
<!-- page.html (HTL/Sightly template) -->
<sly data-sly-use.clientLib="${'/libs/granite/sightly/templates/clientlib.html'}">
<sly data-sly-call="${clientLib.all @ categories='mysite.analytics'}"/>
</sly>
Method 2: HTL Template Direct Injection
For scripts that must load before any clientlib processing, inject directly in the <head> of your base page component:
<!-- /apps/mysite/components/page/head.html -->
<head data-sly-use.page="com.mysite.core.models.PageModel">
<meta charset="UTF-8"/>
<script>
window.digitalData = ${page.dataLayerJson @ context='unsafe'};
</script>
<sly data-sly-use.clientLib="${'/libs/granite/sightly/templates/clientlib.html'}">
<sly data-sly-call="${clientLib.js @ categories='mysite.analytics'}"/>
</sly>
</head>
The @context='unsafe' directive is required to output raw JSON. Use it only for trusted server-generated data.
Method 3: Adobe Launch (Adobe Tags)
AEM integrates natively with Adobe Launch (now Adobe Experience Platform Tags). Configure the integration through AEM Cloud Service configuration:
- Create a Cloud Configuration at
/conf/mysite/settings/cloudconfigs/launch - Map the Launch property ID and environment
- AEM automatically injects the Launch embed script on pages that reference the configuration
/conf/mysite/settings/cloudconfigs/launch/
└── jcr:content
├── libraryUri = "//assets.adobedtm.com/launch-XXXXX.min.js"
└── reportSuiteId = "mysite-prod"
Apply the cloud configuration to your site's page hierarchy:
/content/mysite/jcr:content
└── cq:cloudserviceconfigs = ["/conf/mysite/settings/cloudconfigs/launch"]
Data Layer Implementation
Sling Model Approach
Create a Sling Model that exposes page data as JSON:
// core/src/main/java/com/mysite/core/models/AnalyticsDataLayer.java
@Model(adaptables = SlingHttpServletRequest.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class AnalyticsDataLayer {
@ScriptVariable
private Page currentPage;
@SlingObject
private Resource resource;
@Self
private SlingHttpServletRequest request;
public String getDataLayerJson() {
JsonObject data = new JsonObject();
data.addProperty("platform", "aem");
data.addProperty("pageTitle", currentPage.getTitle());
data.addProperty("pagePath", currentPage.getPath());
data.addProperty("pageTemplate", currentPage.getTemplate().getPath());
data.addProperty("language", currentPage.getLanguage().toString());
data.addProperty("lastModified",
currentPage.getLastModified() != null
? currentPage.getLastModified().toInstant().toString()
: null);
// Component inventory on the page
JsonArray components = new JsonArray();
Resource content = currentPage.getContentResource();
if (content != null) {
collectComponents(content, components);
}
data.add("components", components);
return data.toString();
}
private void collectComponents(Resource resource, JsonArray components) {
String type = resource.getResourceType();
if (type != null && type.startsWith("mysite/")) {
components.add(type);
}
for (Resource child : resource.getChildren()) {
collectComponents(child, components);
}
}
}
Reference it in HTL:
<sly data-sly-use.analytics="com.mysite.core.models.AnalyticsDataLayer">
<script>
window.digitalData = ${analytics.dataLayerJson @ context='unsafe'};
</script>
</sly>
AEM Core Components Data Layer
AEM Core Components (v2.4+) include a built-in data layer. Enable it in the page policy:
{
"dataLayer": {
"enabled": true
}
}
This populates adobeDataLayer on every page with component-level data, including component IDs, types, and content metadata. It uses the Adobe Client Data Layer standard, which integrates directly with Adobe Launch rules.
Editable Templates and Script Injection
AEM's editable templates (introduced in AEM 6.3) define page structure through policies. Assign clientlib categories at the template policy level:
/conf/mysite/settings/wcm/templates/content-page/policies/
└── jcr:content
└── root
└── responsivegrid
└── cq:policy → /conf/mysite/settings/wcm/policies/wcm/foundation/components/page/policy_analytics
The policy node:
<policy_analytics
jcr:primaryType="nt:unstructured"
clientlibs="[mysite.analytics,mysite.base]"
clientlibsJsHead="[mysite.analytics]"/>
This ensures every page using the template includes the analytics clientlib, without requiring authors to add it manually.
Common Issues
Dispatcher Cache Serving Stale Data Layer
The Dispatcher caches full HTML pages. If your data layer contains dynamic values (user segment, A/B test variant), those values are cached with the first response.
Solutions:
- Use Sling Dynamic Include (SDI) to mark the data layer component as uncacheable:
<!-- In the page component HTL -->
<sly data-sly-resource="${'datalayer' @ resourceType='mysite/components/datalayer',
decorationTagName='sly',
addSelectors='nocache'}"/>
Configure the SDI filter in the Dispatcher to fetch that component via ESI or SSI.
- Alternatively, fetch dynamic data client-side:
fetch('/content/mysite/en/jcr:content.analytics.json')
.then(r => r.json())
.then(data => {
window.digitalData = Object.assign(window.digitalData || {}, data);
window.adobeDataLayer.push({ event: 'context-loaded' });
});
Clientlib Not Loading After Deployment
After deploying via Cloud Manager, clientlibs may not appear if:
- The clientlib category name does not match what the template references
- The
js.txtmanifest file is missing or has incorrect paths - The clientlib is not compiled (check for build errors in Cloud Manager logs)
Debug clientlibs by appending ?debugClientLibs=true to any page URL on the author instance, or check /libs/granite/ui/content/dumplibs.html.
HTL Context Filtering Breaks JSON Output
HTL escapes output by default. If your data layer JSON is being HTML-encoded, you are missing the context directive:
<!-- Wrong: JSON will be escaped -->
<script>var data = ${model.json};</script>
<!-- Correct: raw JSON output -->
<script>var data = ${model.json @ context='unsafe'};</script>
Only use unsafe for server-generated, trusted content.
Adobe Launch Firing Before Data Layer
If Launch rules execute before digitalData is populated, the data layer values are undefined. Ensure the data layer script is in <head> before the Launch embed, or use Launch's Library Loaded event with a custom condition that checks for data layer readiness.
Platform-Specific Considerations
AEM as a Cloud Service vs. AEM 6.5 -- Cloud Service deployments are immutable. You cannot modify Dispatcher configuration at runtime; all changes go through Cloud Manager. On 6.5, Dispatcher is managed manually and you have more flexibility with caching rules for analytics endpoints.
Component-level tracking -- AEM's component architecture means each component can emit its own analytics events. Use a consistent pattern: each component's clientlib pushes to adobeDataLayer when the component renders or when a user interacts with it.
// Component-level event in a clientlib
document.querySelectorAll('[data-cmp-type="accordion"]').forEach(el => {
el.addEventListener('click', function(e) {
window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({
event: 'component-click',
component: {
type: 'accordion',
id: el.dataset.cmpId,
title: el.querySelector('.cmp-accordion__title')?.textContent
}
});
});
});
Author vs. Publish -- Analytics should only fire on Publish instances. Check the run mode in your Sling Model or use a clientlib category that is only included in Publish-mode templates. The WCM mode can be checked in HTL:
<sly data-sly-test="${!wcmmode.edit && !wcmmode.preview}">
<!-- Analytics scripts here -->
</sly>
Multisite with Language Copies -- AEM's MSM (Multi-Site Manager) creates language copies that share structure but differ in content. Ensure your data layer includes the hreflang and site section so analytics can segment by locale without cross-contamination.