Реалізація динамічного контенту на лендингу за UTM-мітками
Лендинг з динамічним контентом за UTM — це один URL, який показує різний текст, зображення та офери залежно від рекламної кампанії, яка привела користувача. Замість створення десятків окремих посадкових сторінок для кожної кампанії достатньо настроїти заміну контентних блоків на основі параметрів URL.
Вилучення та збереження UTM-параметрів
Параметри потрібно читати при завантаженні сторінки та зберігати — вони пропадають при переходах за внутрішніми посиланнями:
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;
}
// Повернення — параметрів у URL нема, беремо з сесії
return JSON.parse(sessionStorage.getItem('utm') ?? '{}');
}
const utm = saveAndGetUtm();
Карта замін контенту
Створюється JSON-конфіг (може лежати у файлі, в CMS або в базі) — правила підстановки контенту:
const contentMap = {
// За utm_campaign
campaigns: {
'google-search-brand': {
hero_title: 'Офіційний сайт — найкращий вибір для вашого бізнесу',
hero_subtitle: 'Прямі поставки. Без посередників.',
cta_text: 'Отримати персональну пропозицію',
badge: 'Офіційний партнер',
},
'facebook-retargeting-cart': {
hero_title: 'Ви забули щось важливе',
hero_subtitle: 'Ваша кошик чекає — завершіть замовлення сьогодні.',
cta_text: 'Повернутися до кошика',
cta_url: '/cart',
},
'email-promo-march': {
hero_title: 'Спеціальна пропозиція для підписників',
hero_subtitle: 'Знижка 15% діє до кінця місяця.',
cta_text: 'Активувати знижку',
badge: '-15%',
},
},
// За utm_source (фолбек якщо нема збігу за кампанією)
sources: {
'yandex': {
hero_title: 'Знайшли нас у Яндексі? Правильний вибір.',
cta_text: 'Дізнатися докладніше',
},
'vk': {
hero_title: 'Ласкаво просимо з ВКонтакте',
cta_text: 'Переглянути каталог',
},
},
// Дефолт
default: {
hero_title: 'Рішення для вашого бізнесу',
hero_subtitle: 'Надійно, швидко, без зайвих затрат.',
cta_text: 'Почати роботу',
},
};
function getContent(utm: Record<string, string>) {
return contentMap.campaigns[utm.utm_campaign]
?? contentMap.sources[utm.utm_source]
?? contentMap.default;
}
Заміна DOM-елементів
Атрибути data-utm-key вказують, яке поле контенту підставляти в елемент:
<h1 data-utm-key="hero_title">Рішення для вашого бізнесу</h1>
<p data-utm-key="hero_subtitle">Надійно, швидко, без зайвих затрат.</p>
<a href="/start" data-utm-key="cta_text" data-utm-href="cta_url" id="main-cta">
Почати роботу
</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];
}
});
}
// Запуск при завантаженні
const content = getContent(utm);
applyDynamicContent(content);
Заміна відбувається до першого рендеру, якщо скрипт стоїть в <head> з атрибутом defer, або одразу в кінці <body>. При SSR краще передавати UTM на сервер та рендерити потрібний варіант одразу.
SSR-варіант на PHP/Laravel
// У Blade-шаблоні
@php
$utm = [
'utm_source' => request('utm_source') ?? session('utm_source'),
'utm_campaign' => request('utm_campaign') ?? session('utm_campaign'),
];
// Зберігаємо в сесію при першому посещенні
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-підхід не вирізує миготіння контенту — користувач одразу отримує персоналізовану версію.
Трекінг ефективності варіантів
// Відправляємо в GA4 який варіант бачить користувач
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),
});
// На клік по CTA
document.getElementById('main-cta')?.addEventListener('click', () => {
gtag('event', 'utm_cta_click', {
cta_text: content.cta_text,
utm_campaign: utm.utm_campaign,
});
});
У GA4 будується сегментація за utm_content_variant — видно, які кампанії приводять до кліків по CTA, а які закінчуються відмовою.
Графік
JS-заміна контенту з картою кампаній до 20 варіантів: 4-6 годин. SSR-версія на PHP з сессійним зберіганням UTM: 3-4 години. Трекінг варіантів у GA4 із сегментацією: 2 години. Інтеграція з CMS (редактор варіантів в админке): 1-2 дні.







