Build tracking URLs from a shared prefix
Your tracking convention says every ad’s final URL ends in its own
slug — utm_campaign=GH_Cookies_0708-rsa_a, …-rsa_b, …-sitelink_help —
so no two ads share a URL, but everything before the slug is
identical. Pasting the full URL into every ad is how campaigns end up
shipping with a URL copied from last month’s campaign, quietly
mis-attributing traffic.
Put the shared part in a local and build
each URL from it with ${…} interpolation:
locals { utm_base = "https://www.ghostery.com/ghostery-ad-blocker?utm_source=search&utm_campaign=GH_Cookies_0708"}
resource "google_ads_ad_group_ad" "rsa_a" { ad_group = google_ads_ad_group.popups.id ad { final_urls = ["${local.utm_base}-rsa_a"] responsive_search_ad { headlines = local.popup_headlines descriptions = local.popup_descriptions } }}
resource "google_ads_ad_group_ad" "rsa_b" { ad_group = google_ads_ad_group.popups.id ad { final_urls = ["${local.utm_base}-rsa_b"] responsive_search_ad { headlines = local.consent_headlines descriptions = local.consent_descriptions } }}Change the campaign tag or the landing page once, in the local, and
every ad picks it up. bidsmith plan shows the fan-out as one
in-place update per ad before anything changes live.
Where interpolation works
Anywhere a string is accepted — resource names, headlines, paths,
URLs — including inside locals themselves, so you can build one
local from another:
locals { campaign_tag = "GH_Cookies_0708" landing = "https://www.ghostery.com/ghostery-ad-blocker" utm_base = "${local.landing}?utm_source=search&utm_campaign=${local.campaign_tag}"}
resource "google_ads_campaign" "cookies" { name = "Search — ${local.campaign_tag}" advertising_channel_type = "SEARCH" campaign_budget = google_ads_campaign_budget.cookies.id}Numbers and booleans interpolate too ("radius-${local.km}km").
Validation runs on the rendered string, so a headline that only
exceeds the 30-character cap once the brand name is spliced in still
gets flagged before it reaches Google Ads.
Let Google append the tracking instead
Interpolation keeps the source tidy, but every ad still ships a URL
with the UTM query baked in. Google Ads has a field for exactly this
job: final_url_suffix is a query string Google appends at click time,
so the campaign can declare the convention once and each ad supply only
what varies.
resource "google_ads_campaign" "cookies" { name = "Search — GH_Cookies_0708" advertising_channel_type = "SEARCH" campaign_budget = google_ads_campaign_budget.cookies.id
final_url_suffix = "utm_source=search&utm_campaign=GH_Cookies_0708-{_slug}"}
resource "google_ads_ad_group_ad" "cookies_rsa_a" { ad_group = google_ads_ad_group.cookies.id template = ad_template.cookies_rsa
ad { final_urls = ["https://www.ghostery.com/ghostery-ad-blocker"] custom_parameters = { slug = "rsa_a" } }}{_slug} is a custom parameter: the custom_parameters map fills
it in, and the leading underscore is Google’s, not a typo. Set the pair
on a campaign, an ad group, or an ad — the most specific one wins, so a
campaign-wide convention can be overridden for one ad group without
touching the rest.
The payoff beyond brevity: a convention declared in one place is a
convention you can review. A typo in utm_campaign pasted into 60 URLs
breaks attribution silently and is invisible in a diff; the same typo in
one final_url_suffix is one line in one pull request.
See also
- Reuse a headline set across many ads — the same idea for whole lists.
- Locals — how references resolve across files.