Dynamic Landing Content by UTM Tags Implementation
UTM-based dynamic landing is a single URL showing different text, images, and offers depending on advertising campaign that brought the user. Instead of creating dozens of landing pages for each campaign, just configure content block replacements based on URL parameters.
Extracting and Storing UTM Parameters
Read parameters on page load and save them—they disappear during internal link transitions:
function extractUtmParams(): Record<string, string> {
const params = new URLSearchParams(location.search);
const utm: Record<string, string> = {};
['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'].forEach(key => {
const val = params.get(key);
if (val) utm[key] = val;
});
return utm;
}
function saveAndGetUtm(): Record<string, string> {
const fromUrl = extractUtmParams();
if (Object.keys(fromUrl).length > 0) {
sessionStorage.setItem('utm', JSON.stringify(fromUrl));
return fromUrl;
}
// Return visit—no parameters in URL, get from session
return JSON.parse(sessionStorage.getItem('utm') ?? '{}');
}
const utm = saveAndGetUtm();
Content Replacement Map
Create JSON config (file, CMS, or database)—content substitution rules:
const contentMap = {
// By utm_campaign
campaigns: {
'google-search-brand': {
hero_title: 'Official site—best choice for your business',
hero_subtitle: 'Direct supplies. No middlemen.',
cta_text: 'Get personal offer',
badge: 'Official partner',
},
'facebook-retargeting-cart': {
hero_title: 'You forgot something important',
hero_subtitle: 'Your cart is waiting—complete order today.',
cta_text: 'Back to cart',
cta_url: '/cart',
},
'email-promo-march': {
hero_title: 'Special offer for subscribers',
hero_subtitle: '15% discount valid until month end.',
cta_text: 'Activate discount',
badge: '-15%',
},
},
// By utm_source (fallback if no campaign match)
sources: {
'yandex': {
hero_title: 'Found us on Yandex? Right choice.',
cta_text: 'Learn more',
},
'vk': {
hero_title: 'Welcome from VKontakte',
cta_text: 'View catalog',
},
},
// Default
default: {
hero_title: 'Solution for your business',
hero_subtitle: 'Reliable, fast, no extra costs.',
cta_text: 'Get started',
},
};
function getContent(utm: Record<string, string>) {
return contentMap.campaigns[utm.utm_campaign]
?? contentMap.sources[utm.utm_source]
?? contentMap.default;
}
DOM Element Replacement
data-utm-key attributes indicate which content field to substitute:
<h1 data-utm-key="hero_title">Solution for your business</h1>
<p data-utm-key="hero_subtitle">Reliable, fast, no extra costs.</p>
<a href="/start" data-utm-key="cta_text" data-utm-href="cta_url" id="main-cta">
Get started
</a>
<span class="badge" data-utm-key="badge" data-utm-hide-if-empty="true"></span>
function applyDynamicContent(content: Record<string, string>): void {
document.querySelectorAll('[data-utm-key]').forEach(el => {
const key = el.getAttribute('data-utm-key')!;
const value = content[key];
if (!value) {
if (el.getAttribute('data-utm-hide-if-empty') === 'true') {
(el as HTMLElement).style.display = 'none';
}
return;
}
el.textContent = value;
const hrefKey = el.getAttribute('data-utm-href');
if (hrefKey && content[hrefKey]) {
(el as HTMLAnchorElement).href = content[hrefKey];
}
});
}
// Run on load
const content = getContent(utm);
applyDynamicContent(content);
Substitution happens before first render if script in <head> with defer, or right at </body> end. With SSR better to pass UTM to server and render correct variant immediately.
PHP/Laravel SSR Variant
// In Blade template
@php
$utm = [
'utm_source' => request('utm_source') ?? session('utm_source'),
'utm_campaign' => request('utm_campaign') ?? session('utm_campaign'),
];
// Save to session on first visit
if (request('utm_source')) {
session(['utm_source' => request('utm_source')]);
session(['utm_campaign' => request('utm_campaign')]);
}
$content = App\Services\UtmContentService::resolve($utm);
@endphp
<h1>{{ $content['hero_title'] }}</h1>
<p>{{ $content['hero_subtitle'] }}</p>
<a href="{{ $content['cta_url'] ?? '/start' }}">{{ $content['cta_text'] }}</a>
SSR approach avoids content flash—user gets personalized version immediately.
Tracking Variant Effectiveness
// Send to GA4 which variant user sees
gtag('event', 'utm_content_variant', {
utm_campaign: utm.utm_campaign ?? 'none',
utm_source: utm.utm_source ?? 'direct',
variant_key: utm.utm_campaign ?? utm.utm_source ?? 'default',
hero_title: content.hero_title?.substring(0, 50),
});
// On CTA click
document.getElementById('main-cta')?.addEventListener('click', () => {
gtag('event', 'utm_cta_click', {
cta_text: content.cta_text,
utm_campaign: utm.utm_campaign,
});
});
In GA4 segmentation by utm_content_variant shows which campaigns drive CTA clicks and which end in bounces.
Timeline
JS content replacement with up to 20 campaign variants: 4-6 hours. SSR version in PHP with session UTM storage: 3-4 hours. GA4 variant tracking with segmentation: 2 hours. CMS integration (variants editor in admin): 1-2 days.







