search ]

Transfer UTM Parameters across Pages & Domains with JS

In one of my recent projects, the client asked me to forward UTM parameters when a user clicks a link that leads to a web application on a different domain. The goal was to preserve campaign attribution across the domain boundary so GA4 could tie conversions back to the original traffic source.

This is a common need, especially on landing pages where a CTA points to an external app or checkout flow. I’ll share the script I use and explain how it works.

What Does the Script Do?

The script scans every link on the page. When it finds one pointing to a specific external domain, it checks whether UTM parameters are already present.

If not, it appends the UTM parameters from the current page URL to that link.

An example makes this clearer. Say a user lands on a page with these parameters:

https://lp.com/?utm_source=google&utm_medium=cpc&utm_campaign=name-campaign&utm_term=keyword&utm_content=fb-banner

And the landing page has a CTA linking to an external application:

https://app.com/

The script rewrites that link to:

https://app.com/?utm_source=google&utm_medium=cpc&utm_campaign=name-campaign&utm_term=keyword&utm_content=fb-banner

The Script

(function () {
    const targetDomain = "app.co.il";
    const pageParams = new URLSearchParams(window.location.search);
    const utmEntries = [...pageParams.entries()].filter(([key]) => key.startsWith("utm_"));

    if (utmEntries.length === 0) return;

    document.querySelectorAll("a[href]").forEach(link => {
        try {
            const url = new URL(link.href);
            if (!url.hostname.includes(targetDomain)) return;

            utmEntries.forEach(([key, value]) => {
                if (!url.searchParams.has(key)) {
                    url.searchParams.set(key, value);
                }
            });

            link.href = url.toString();
        } catch (e) {}
    });
})();

How It Works

  • Line 2 – set targetDomain to the external domain you want to forward UTMs to. Only links matching this hostname will be modified.
  • Lines 3-4 – read the current page URL and extract all parameters that start with utm_.
  • Line 6 – if no UTM parameters are present, the script exits immediately.
  • Lines 8-18 – loop through every <a> on the page. For each one, parse it with the URL constructor, check the hostname, and use searchParams.set() to append missing UTMs.

The script uses the URL API instead of string concatenation. This matters because searchParams.set() automatically handles URL encoding, avoids duplicate parameters, and correctly places query parameters before any hash fragment.

If you’re using Google Tag Manager, add this script as a Custom HTML tag and set it to fire on the DOM Ready trigger. Otherwise, place the script at the end of the page body.

Important Notes

  • The script only affects links to the specified targetDomain. All other links remain untouched.
  • Links that already contain UTM parameters are skipped – the script won’t overwrite existing attribution.
  • The try/catch block silently skips mailto:, tel:, and other non-HTTP links that would throw when parsed by the URL constructor.
  • The script runs once on page load. If your page dynamically adds links after load (SPA, AJAX), you’ll need to call the function again after the new links are inserted.

Persisting UTMs Across Page Navigations

The script above works when UTM parameters are present in the current page URL. But what if the user lands on your homepage with UTMs, navigates to an inner page, and only then clicks the CTA to the external app?

The UTMs are lost because the inner page URL doesn’t contain them.

You can solve this with sessionStorage. The enhanced version stores UTMs when the user first arrives and applies them on every subsequent page:

(function () {
    const targetDomain = "app.co.il";
    const utmKeys = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
    const params = new URLSearchParams(window.location.search);
    const freshUtms = utmKeys.filter(key => params.has(key));

    if (freshUtms.length > 0) {
        utmKeys.forEach(key => sessionStorage.removeItem(key));
        freshUtms.forEach(key => sessionStorage.setItem(key, params.get(key)));
    }

    const utms = utmKeys
        .filter(key => sessionStorage.getItem(key))
        .map(key => [key, sessionStorage.getItem(key)]);

    if (utms.length === 0) return;

    document.querySelectorAll("a[href]").forEach(link => {
        try {
            const url = new URL(link.href);
            if (!url.hostname.includes(targetDomain)) return;

            utms.forEach(([key, value]) => {
                if (!url.searchParams.has(key)) {
                    url.searchParams.set(key, value);
                }
            });

            link.href = url.toString();
        } catch (e) {}
    });
})();

sessionStorage persists for the duration of the browser tab. Once the user closes the tab, the data is cleared.

This is the right scope for campaign attribution – you don’t want UTMs from last week’s visit overriding today’s.

Never add UTM parameters to internal links. UTMs on internal links override the original traffic source in GA4 and create false sessions, making your campaign data unreliable.

FAQs

Does this script work with GA4?
Yes. GA4 reads UTM parameters from the landing page URL just like Universal Analytics did. The five standard parameters - utm_source, utm_medium, utm_campaign, utm_term, and utm_content - all map directly to GA4 session dimensions. The script preserves these parameters when forwarding to an external domain.
Why use the URL API instead of string concatenation?
The URL constructor and URLSearchParams handle encoding automatically, prevent duplicate parameters, and correctly place query strings before hash fragments. String concatenation with += can break URLs that contain a # hash, because the parameters end up in the fragment instead of the query string.
What browsers support URLSearchParams?
URLSearchParams is supported in Chrome 49+, Firefox 44+, Safari 10.1+, and Edge 17+. It covers over 98% of global browser traffic. Internet Explorer is the only notable exception, but IE usage is effectively zero in 2026.
Should I use sessionStorage or localStorage for UTM persistence?
Use sessionStorage. It clears when the user closes the browser tab, which aligns with how analytics sessions work. localStorage persists indefinitely, which means a user returning days later would still carry old UTM values that no longer reflect their actual traffic source.
Can I forward UTMs to multiple external domains?
Yes. Replace the single targetDomain string with an array of domains and check against all of them. For example: const targetDomains = ["app.co.il", "checkout.co.il"]; then use targetDomains.some(d => url.hostname.includes(d)) instead of the single includes() check.
Why shouldn't I add UTM parameters to internal links?
UTM parameters on internal links override the original traffic source in GA4 and start a new session. A user who arrived from a Google Ads campaign would appear as coming from your internal link instead. This destroys attribution data and inflates session counts, making campaign reporting unreliable.

Comments and questions are welcome – always happy to hear ideas for improving these snippets 🙂

Join the Discussion
0 Comments  ]

Leave a Comment

To add code, use the buttons below. For instance, click the PHP button to insert PHP code within the shortcode. If you notice any typos, please let us know!

Savvy WordPress Development official logo