Skip to main content

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 messageReceived event 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)

  1. Go to your ai12z‑cta configScriptCustom JS.
  2. Paste the following script and adjust the utmParams value as needed.
  3. 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 messageReceived with detail.domId that 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 a utm_ query key.
  • A MutationObserver re-applies the logic whenever new nodes are added (e.g., follow‑ups, incremental rendering).

Customization

  • Change the UTM string: Edit the utmParams constant 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.domId and 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.log calls 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