Landing Headline Substitution by Ad Campaign
Headline substitution (dynamic keyword insertion at site level) is a technique where H1 and subheadings automatically change to match the ad that brought the user. If person clicked ad "Website design for dentistry"—they land on page with heading "Website design for dentistry", not generic web studio landing.
Operating Principle
Ad system passes parameters in URL. Either standard UTM or custom parameters. Site reads parameters, finds correct headline in dictionary, and substitutes it in DOM before user can see original text.
Two approaches: parameter in URL directly contains text (unsafe—can be spoofed) or parameter contains key where server/client finds headline from predefined dictionary.
Headline Dictionary
// headline-map.js — campaign → headline mapping
export const headlineMap: Record<string, HeadlineSet> = {
// Google Ads — by utm_campaign
'dental-clinic-sites': {
h1: 'Websites for dental clinics—turnkey in 14 days',
h2: 'Online booking, patient portal, MIS integration',
breadcrumb: 'Clinic websites',
},
'lawyer-sites': {
h1: 'Websites for law firms',
h2: 'Privacy, trust, consultation booking form',
breadcrumb: 'Lawyer websites',
},
'ecom-landing': {
h1: 'E-commerce from scratch—prototype to launch',
h2: 'Catalog, cart, payment, shipping—all ready',
breadcrumb: 'E-commerce stores',
},
// By utm_term (keyword)
terms: {
'create website': {
h1: 'We'll create a website for your needs',
h2: 'Calculate cost in 2 minutes',
},
'landing page development': {
h1: 'Landing with 5%+ conversion',
h2: 'Sells 24/7—while you focus on business',
},
},
};
Substitution Logic
interface HeadlineSet {
h1?: string;
h2?: string;
breadcrumb?: string;
}
function resolveHeadlines(): HeadlineSet {
const params = new URLSearchParams(location.search);
const campaign = params.get('utm_campaign') ?? sessionStorage.getItem('utm_campaign');
const term = params.get('utm_term') ?? sessionStorage.getItem('utm_term');
// Save for next pages
if (params.get('utm_campaign')) {
sessionStorage.setItem('utm_campaign', params.get('utm_campaign')!);
sessionStorage.setItem('utm_term', params.get('utm_term') ?? '');
}
// Priority: campaign > keyword > default
if (campaign && headlineMap[campaign]) return headlineMap[campaign];
if (term) {
const termKey = Object.keys(headlineMap.terms ?? {})
.find(k => term.toLowerCase().includes(k));
if (termKey) return headlineMap.terms[termKey];
}
return {}; // don't change default text
}
function applyHeadlines(headlines: HeadlineSet): void {
const map: [string, keyof HeadlineSet][] = [
['[data-headline="h1"]', 'h1'],
['[data-headline="h2"]', 'h2'],
['[data-headline="breadcrumb"]', 'breadcrumb'],
];
map.forEach(([selector, key]) => {
const el = document.querySelector(selector);
if (el && headlines[key]) {
el.textContent = headlines[key]!;
}
});
}
// Run—ideally inline in <head> to avoid flicker
const headlines = resolveHeadlines();
applyHeadlines(headlines);
HTML Markup
<!DOCTYPE html>
<html>
<head>
<!-- Headline substitution script—before body render -->
<script src="/js/headline-substitution.js"></script>
</head>
<body>
<nav class="breadcrumb">
<a href="/">Home</a> /
<span data-headline="breadcrumb">Website development</span>
</nav>
<section class="hero">
<h1 data-headline="h1">Website development for business</h1>
<p data-headline="h2">Since 2010 — over 400 projects</p>
<a href="/brief" class="btn-primary">Calculate cost</a>
</section>
</body>
</html>
Script substitution must be synchronous in <head>, not defer/async—otherwise user sees original heading for millisecond before replacement (flash of original content). Script is small—1-2 KB, render blocking insignificant.
Google Ads Dynamic Keyword Insertion vs Headline Substitution
DKI in Google Ads inserts keyword into ad. Headline substitution on site—logical continuation: site "answers" in same language as ad. Connection works via utm_term:
Google Ads campaign creates URL template:
https://example.com/landing?utm_campaign=dental&utm_term={keyword}
Google auto-substitutes actual keyword into {keyword}. Site reads utm_term and shows corresponding headline.
Server-Side Substitution (No Flicker)
For SSR projects, server-side substitution is more reliable:
// LandingController.php
public function index(Request $request): Response
{
$campaign = $request->get('utm_campaign') ?? $request->session()->get('utm_campaign');
$term = $request->get('utm_term') ?? $request->session()->get('utm_term');
if ($request->get('utm_campaign')) {
$request->session()->put('utm_campaign', $campaign);
$request->session()->put('utm_term', $term);
}
$headlines = HeadlineResolver::resolve($campaign, $term);
return Inertia::render('Landing', [
'headlines' => $headlines,
]);
}
Substitution Correctness Check
For QA, special mode: adding ?debug_headlines=1 to URL shows all available headline variants as table—convenient for testing before campaign launch.
Timeline
Headline dictionary + JS client-side substitution: 3-5 hours. Server-side substitution with session + Inertia: 4-6 hours. QA mode for checking all variants: 1-2 hours. Loading dictionary from CMS (editable list in admin): 1 day.







