Example: Per-Content Conversions
Traditional sites only
This example relies on the injected script exposing window.dotAnalytics on the page. In a headless app the same idea works — read the two fields off the contentlet and call conversion() from the hook — but the code below is not portable as written.
By default, firing a conversion means a developer hardcodes the call and redeploys. Every new goal, every renamed conversion, every "can we also track this one?" goes back into the development queue.
This example moves that decision to the people making it. A developer adds two fields to a Content Type and drops a script into the container once. From then on editors decide, per content instance, whether it counts as a conversion and what that conversion is called — no code changes, no redeploy.
The worked example is a CTA button inside a Hero, a reusable Content Type placed on many pages through a container. The same technique applies to any reusable Content Type.
Avoiding False Positives#
Everything here exists to fire conversion(name) at exactly the right moment and never at the wrong one. A false positive is worse than a missing conversion: it inflates your numbers.
| Guard | Prevents |
|---|---|
| Success gate — fire only after the goal succeeds | Counting intent instead of completion. For a bare CTA the click is completion — the exception, not the rule. |
Editor / preview guard — if (window.dotAnalytics) | Recording conversions while an author edits or previews inside UVE. |
| Double-fire guard | The same goal counted twice from a double-click or re-submit. |
| Enabled + named guard | Firing on instances the editor never opted in, or with an empty name. |
Step 1: Add Two Fields#
| Field | Type | Variable | Purpose |
|---|---|---|---|
| Enable Conversion | Checkbox | enableConversion | Turns tracking on for this instance. |
| Conversion name | Select (preferred) or Text | conversionName | The name reported to analytics. |
Prefer a Select over free text
Conversions aggregate by name, so booking, Booking and book-ing become three different conversions and fragment your dashboard. Note that existing content will not have these values until it is re-saved — new fields are not back-filled.
Step 2: Detect and Fire#
#set($doConversion = false)
#if($UtilMethods.isSet($conversionName) && "$!{enableConversion}" == "true")
#set($doConversion = true)
#end
<a class="primary-cta" href="$cta1Url"#if($doConversion) data-dot-conversion="$esc.html($!{conversionName})"#end>$cta1Text</a>
#if($doConversion)
<script>
(function () {
if (window.__dotConversionListenerAdded) return;
window.__dotConversionListenerAdded = true;
document.addEventListener('click', function (event) {
var link = event.target.closest('a[data-dot-conversion]');
if (!link) return;
var name = link.getAttribute('data-dot-conversion');
if (!name) return;
if (!window.dotAnalytics) return; // editor / analytics off
window.dotAnalytics.conversion(name);
});
})();
</script>
#endWhy there is no setTimeout or preventDefault
On Traditional pages, following a link is a real page unload. The SDK flushes pending events on pagehide with keepalive, so the conversion is delivered even though you navigate immediately. Holding navigation only adds lag.
Adapting to Other Goals#
| Goal type | Success moment | Where to fire |
|---|---|---|
| CTA / link click | The click itself | On click, then let it navigate |
| Form submission | After the submit succeeds | In the submit success handler |
| Purchase / checkout | After payment is confirmed | After the payment API returns success |
| Gated download | After the download starts | In the download success callback |