Append UTM Parameters to Links in <ai12z-cta> Search Results
Context: Marketers would like to append analytics UTM parameters to links suggested by the ai12z chatbot/CTA. This guide shows a reliable method that does not rely on prompt engineering and works at render time within the component.
What this does
- Listens for the
messageReceivedevent from the<ai12z-cta>component - Locates the rendered response container in the component's Shadow DOM (including nested
<ai12z-chat>if present) - Appends a fixed UTM string to every
<a href>inside that container - Re-applies automatically whenever the DOM updates (e.g., follow‑up answers, new suggestions)
- Skips links that already contain any
utm_parameter
When to use this
Use this approach when you want consistent, reliable analytics tagging on all links displayed by the CTA/chat—regardless of the prompt content or model output. It is ideal for Proof‑of‑Concepts and production sites where link tracking is required.
Prerequisites
- ai12z web control:
<ai12z-cta>(and/or nested<ai12z-chat>) is installed and working - Access to Config → Script → Custom JS for your CTA instance
- A fixed UTM string you want to append (example included below)
Quick start (copy‑paste)
- Go to your ai12z‑cta config → Script → Custom JS.
- Paste the following script and adjust the
utmParamsvalue as needed. - Save and publish your CTA.
// ---- Begin: Append UTM parameters to all links rendered by ai12z-cta / ai12z-chat ----
const utmParams = "utm_source=newsletter&utm_medium=email&utm_campaign=launch"
ai12zCta.addEventListener("messageReceived", (event) => {
event.stopImmediatePropagation() // stop duplicates
console.log("event received")
if (!event.detail.domId) {
console.warn("No domId provided in messageReceived event")
return
}
let domEle = null
// Try CTA shadow DOM
domEle = ai12zCta.shadowRoot?.querySelector(`#${event.detail.domId}`)
console.log("Found in CTA shadow DOM:", domEle)
// Try nested chat component
if (!domEle) {
const chatElement = ai12zCta.shadowRoot?.querySelector("ai12z-chat")
if (chatElement) {
domEle = chatElement.shadowRoot?.querySelector(`#${event.detail.domId}`)
console.log("Found in Chat shadow DOM:", domEle)
}
}
if (!domEle) {
console.warn(`Element with ID ${event.detail.domId} not found, retrying...`)
setTimeout(() => {
const retryEle =
ai12zCta.shadowRoot?.querySelector(`#${event.detail.domId}`) ||
ai12zCta.shadowRoot
?.querySelector("ai12z-chat")
?.shadowRoot?.querySelector(`#${event.detail.domId}`)
if (retryEle) {
setupUTMUpdater(retryEle)
} else {
console.error(`Element with ID ${event.detail.domId} still not found.`)
}
}, 500)
return
}
setupUTMUpdater(domEle)
})
function setupUTMUpdater(container) {
if (!container) return
console.log("Setting up UTM updater for:", container)
// Run once initially
applyUTMParams(container, utmParams)
// Watch for future DOM changes
const observer = new MutationObserver((mutations) => {
let addedNodes = false
for (const m of mutations) {
if (m.addedNodes.length > 0) {
addedNodes = true
break
}
}
if (addedNodes) {
console.log("Detected DOM change — reapplying UTM params")
applyUTMParams(container, utmParams)
}
})
observer.observe(container, { childList: true, subtree: true })
}
function applyUTMParams(container, utmParams) {
if (!container) return
const links = container.querySelectorAll("a")
links.forEach((link) => {
try {
// Skip if already has UTM params
if (link.href.includes("utm_")) return
const separator = link.href.includes("?") ? "&" : "?"
const newHref = link.href + separator + utmParams
// Update the actual href attribute
link.setAttribute("href", newHref)
console.log("Updated href:", newHref)
} catch (err) {
console.warn("Failed to update link:", link, err)
}
})
}
// ---- End: Append UTM parameters ----
How it works
- The CTA emits
messageReceivedwithdetail.domIdthat points to the current answer container. - The script queries inside the CTA's Shadow DOM (and nested
<ai12z-chat>when applicable) to find that container. - It applies UTM parameters to every
<a>if the link does not already include autm_query key. - A
MutationObserverre-applies the logic whenever new nodes are added (e.g., follow‑ups, incremental rendering).
Customization
- Change the UTM string: Edit the
utmParamsconstant to match your campaign tagging standards:const utmParams = "utm_source=chatbot&utm_medium=cta&utm_campaign=apac-poc" - Target only specific links: Replace
container.querySelectorAll("a")with a narrower selector, e.g.a[data-track]. - Prevent appending on certain domains: Add a guard before updating
href:const blocklist = ["example.com", "intranet.local"]
if (blocklist.some((d) => link.hostname.endsWith(d))) return
Troubleshooting
- No UTM added: Ensure the event provides a valid
detail.domIdand that the container actually contains<a>elements. - Double appending: The script checks for
utm_and won't add again. If you use other naming (e.g.,cid=), adjust the guard. - Timing issues: The script retries once (500 ms) if the container isn't found yet. Increase the delay if your theme renders slowly.
- Console noise: You can remove
console.logcalls after validation.
Notes
- Works for both ai12z‑cta and nested ai12z‑bot render paths.
- This method is independent of prompt text and therefore more reliable than instructing the model to add UTM codes itself.
- Consider coordinating with your analytics team on the exact UTM taxonomy to avoid fragmented reporting.
Last updated: 2025-11-12