Article Information

Category: Marketing

Published: 7 March, 2026

Author: Chris de Gruijter

Reading Time: 9 min

Tags

Google AdsValueTracklanding page optimizationpersonalizationCRO
Google Ads digital marketing dashboard on laptop

Dynamic Landing Pages with Google Ads ValueTrack Parameters

Published: 7 March, 2026

One of the most underused levers in Google Ads is also one of the simplest: ValueTrack parameters. These URL parameters let Google automatically append data about each click to your landing page URL — the user's physical location, the keyword that triggered the ad, the device they're on, and more.

Most advertisers set them up for tracking and stop there. But here's what you can actually do with them: dynamically personalize the landing page copy for every visitor based on where they are and what they searched for. Someone in Amsterdam searching "chimney sweep Amsterdam" lands on a page that mentions Amsterdam. Someone in Rotterdam searching "schoorsteen reinigen Rotterdam" sees Rotterdam. Same URL, same page structure — completely different experience.

I've built this system in production for a lead generation website and the conversion rate impact was significant. Here's the full implementation breakdown.

What Are ValueTrack Parameters?

ValueTrack parameters are placeholders you add to your Google Ads tracking template. When someone clicks your ad, Google automatically replaces those placeholders with real data from that specific click.

Your tracking template looks like this:

{lpurl}?location={loc_physical_ms}&keyword={keyword}&device={device}&network={network}

When someone clicks the ad, their browser lands on something like:

https://yoursite.com/?location=1010304&keyword=chimney+sweep&device=m&network=g

The key parameters for personalization are:

  • {loc_physical_ms} — Google Criteria ID of the user's physical location. Maps to a specific city or region.
  • {keyword} — The exact keyword that triggered the ad (URL-encoded). Tells you what the user was searching for.
  • {device} — Returns "m" (mobile), "c" (computer), or "t" (tablet).
  • {network} — Returns "g" (Google search), "s" (search partner), or "d" (display).
  • {gclid} — Google Click ID, useful for offline conversion tracking.

The Personalization Architecture

The full data flow for location and keyword personalization works like this:

  1. User clicks ad → lands on URL with ?location=1010304&keyword=chimney+sweep
  2. On page load, a client-side script reads the URL parameters
  3. The location ID is mapped to a city name using Google's geotargets lookup data
  4. Both the city name and keyword are stored in localStorage with a 7-day expiry
  5. A React/Vue context provider exposes these values to every component on the page
  6. Components conditionally render personalized text when values are present
  7. On subsequent page navigations (no URL params), values are read from localStorage

The localStorage persistence is critical. Users often click an ad, browse a few pages, then convert. If you only read from the URL, you lose the context the moment they navigate to your contact or pricing page.

Mapping Location IDs to City Names

Google's geotarget IDs are documented in their Geotargets CSV. You download the CSV, filter to the countries you're targeting, and convert it to a lookup map. For the Netherlands and Belgium combined, this is roughly 2,000 entries — about 50KB of JSON, ~15KB gzipped. Bundling it directly into the client build is perfectly fine — no API call needed at runtime.

geotargets.json (excerpt)
{
  "1010304": "Assen",
  "1010305": "Amsterdam",
  "1010306": "Rotterdam",
  "1010307": "Utrecht",
  "20002": "Brussels",
  "20003": "Antwerp"
}

Capturing and Storing the Parameters

Create a utility module that handles reading, mapping, and persisting the ad parameters. The pattern is the same whether you're in a React, Vue, or plain JS project:

adParamsStorage.js
import geotargets from '../data/geotargets.json';

const STORAGE_KEY = 'ad_params';
const EXPIRY_DAYS = 7;

export function getAdParams() {
  // 1. Try URL params first (fresh click)
  const params = new URLSearchParams(window.location.search);
  const locationId = params.get('location');
  const keyword = params.get('keyword');

  if (locationId || keyword) {
    const cityName = locationId ? geotargets[locationId] || null : null;
    const normalized = keyword ? keyword.replace(/\+/g, ' ') : null;
    saveAdParams({ cityName, keyword: normalized, locationId });
    return { cityName, keyword: normalized };
  }

  // 2. Fall back to localStorage
  return getStoredAdParams();
}

function saveAdParams(params) {
  const entry = {
    ...params,
    expires: Date.now() + EXPIRY_DAYS * 24 * 60 * 60 * 1000,
  };
  localStorage.setItem(STORAGE_KEY, JSON.stringify(entry));
}

function getStoredAdParams() {
  const raw = localStorage.getItem(STORAGE_KEY);
  if (!raw) return { cityName: null, keyword: null };

  const stored = JSON.parse(raw);
  if (Date.now() > stored.expires) {
    localStorage.removeItem(STORAGE_KEY);
    return { cityName: null, keyword: null };
  }

  return { cityName: stored.cityName, keyword: stored.keyword };
}

Exposing Values Through a Context Provider

Wrap your app with a provider that calls getAdParams() once on mount and exposes the values everywhere. Here's the React version — the Vue/Nuxt equivalent is a composable initialized in a client-only plugin:

AdParamsContext.js
import { createContext, useContext, useEffect, useState } from 'react';
import { getAdParams } from '../common/adParamsStorage';

const AdParamsContext = createContext({
  cityName: null,
  keyword: null,
  isPersonalized: false,
});

export function AdParamsProvider({ children }) {
  const [adParams, setAdParams] = useState({ cityName: null, keyword: null });

  useEffect(() => {
    const params = getAdParams();
    setAdParams(params);
  }, []);

  return (
    <AdParamsContext.Provider value={{ ...adParams, isPersonalized: !!adParams.cityName }}>
      {children}
    </AdParamsContext.Provider>
  );
}

export const useAdParams = () => useContext(AdParamsContext);

Personalizing Components

With the context in place, personalization in any component is a simple conditional. The key design principle: default text always works for organic visitors. Personalized text only appears when context is available — search engines always see the default version.

HeroSection.js
export function HeroSection() {
  const { cityName, keyword, isPersonalized } = useAdParams();

  return (
    <section>
      {keyword && (
        <span className="keyword-badge">Looking for {keyword}?</span>
      )}
      <h1>Expert Services You Can Trust</h1>
      <p>
        {isPersonalized
          ? `Certified professionals serving ${cityName} and surrounding areas.`
          : 'Certified professionals serving your area.'
        }
      </p>
    </section>
  );
}

Apply the same pattern across your pricing section, contact page, testimonials header, and any trust-building copy. Each personalization touch is small on its own — but collectively they create a page that feels tailor-made for that visitor's search.

Closing the Loop: Sending Ad Data to Your CRM

The real power comes when you pass the ad data with every form submission. This lets you analyze which keywords and cities actually convert — not just which ones get clicks.

formSubmitHandler.js
const adParams = getStoredAdTracking();

const submission = {
  name: formData.name,
  email: formData.email,
  message: formData.message,
  // Ad attribution
  ad_keyword: adParams.keyword,
  ad_city: adParams.cityName,
  ad_device: adParams.device,
  ad_campaign_id: adParams.campaignId,
  gclid: adParams.gclid, // For Google Ads offline conversion import
};

await fetch('/api/submit', {
  method: 'POST',
  body: JSON.stringify(submission),
});

With this data in your CRM, you can answer questions like: "Which keywords have the highest lead-to-close rate?" and "Which cities generate the most qualified enquiries?" This feeds directly back into your Google Ads bidding strategy.

Important Caveats

  • Flash of default content: Since personalization happens client-side after hydration, users briefly see the default text. Use CSS opacity transitions to make the swap smooth.
  • Firefox Enhanced Tracking Protection strips the gclid parameter from URLs. Native Google Ads conversion tracking still works via cookies — this only affects custom gclid capture.
  • SEO always sees default text: Search engines crawl the non-personalized version of your page. This is correct — you don't want geo-specific text indexed.
  • GDPR: Storing click data in localStorage for 7 days relates to the current session context. Review with your legal team for your specific jurisdiction and consent setup.

Frequently Asked Questions

Do ValueTrack parameters work with all Google Ads campaign types?

Yes — Search, Display, Performance Max, and Shopping campaigns all support ValueTrack parameters in the tracking template. Note that {loc_physical_ms} and {keyword} are only available for Search campaigns. For Performance Max, use campaign-level parameters.

What happens if the location ID isn't in my geotargets lookup?

Return null for the city name and have your components fall back to default text. This happens for locations Google hasn't assigned to a specific city (e.g., rural areas mapped to a broader region). The fallback keeps the page working correctly for all visitors.

Can I use this with a static site (no server-side rendering)?

Yes — this is entirely client-side. The geotargets lookup runs in the browser, localStorage stores the values, and personalization happens via JavaScript after hydration. It works with Next.js static export, Nuxt generate, or any static hosting setup.

How do I measure whether personalization actually improved conversions?

Push a custom GA4 event when personalized content is displayed (when cityName is not null). Compare conversion rates between personalized and non-personalized sessions. You can also use this as a segment in your Google Ads reports to measure the CPA delta.