Same Page, a Thousand URLs
You probably run some caching already. A cache plugin, server-level page cache, maybe Cloudflare in front of all of it. And it probably handles marketing parameters correctly. Probably.
Here’s the problem. Every campaign link points at the same page but carries a unique query string:
/product/mug/?utm_source=newsletter&utm_medium=email
/product/mug/?utm_source=facebook&fbclid=AbC123xYz
/product/mug/?gclid=EAIaIQobChMI...
To you that’s one product page. What a cache does with those three URLs depends entirely on its configuration — and both failure modes hurt:
- The most common one: cache bypass. Plenty of stacks treat any query string as “dynamic — don’t cache” and send every campaign click straight to PHP. Your homepage serves from cache in milliseconds; the exact same page with
?fbclid=...renders from scratch, uncached. Unless a campaign parameter actually changes what appears on the page — andutm_sourcenever does — you’re burning server time re-rendering the identical page, click after click. That’s the whole idea of caching, inverted: this is precisely the work a cache exists to save. - The other extreme: cache-key explosion. An over-aggressive setup that includes the full query string in the cache key will happily store a hundred variants of the same page. And because click IDs like
fbclidandgclidare unique per click, virtually none of those entries will ever be served twice — every visitor gets a cold miss anyway, and the cache dutifully stores a copy nobody will ever read. You pay for the render and for storing its useless result.
Either way, the pain arrives exactly when you can least afford it: during a newsletter send or a live campaign, when thousands of visitors show up within minutes — all carrying parameters.
And with paid traffic this stops being untidy and starts being expensive. I’ve watched it happen in paid-ads work more than once: every click from Google Ads or Meta arrives with a gclid or fbclid attached, so if parameterized URLs bypass your cache, the visitors you pay the most for are precisely the ones who never hit it. You’re paying real CPCs to serve your slowest, uncached page — to cold traffic, at the moment of highest intent.
Good cache plugins ship an ignore-list for the well-known params. But your stack isn’t one cache — it’s Cloudflare, then a server-level page cache, then whatever your plugin does. Do all of those layers agree on all of the parameters — not just utm_source, but gbraid, wbraid, ttclid, _kx, mc_eid? If you want to answer “probably” with “certainly”, cut the problem off before it reaches any of them.
The Fix: Strip Them at the Edge
I published a small repo with a single Cloudflare Transform Rule that does exactly this: cfrules — Strip Tracking Parameters from URLs. It’s a tip I’ve been showing around for a while — including during my workshop at WordCamp Europe — and somehow never got around to writing down. So here it is, click by click.
It’s two copy-paste expressions:
- an If (filter expression) that fires only when the query string contains a known tracking parameter,
- a Then (a
regex_replace()on the query) that removes every tracking parameter and keeps the legitimate ones.
A request for /blog/post?id=42&utm_source=newsletter&fbclid=AbC123 becomes /blog/post?id=42 — before Cloudflare’s cache, before your server, before anything downstream. One cache entry per page instead of one per campaign link. Tracking junk stops appearing in your server logs. No plugin, no .htaccess, no origin code.
It works on the Free plan, and setup is genuinely about 10 minutes — the README walks through every dashboard click, explains the regex piece by piece, and includes test URLs plus the Cloudflare Trace tool to verify the rule fires.
”Wait — Won’t This Kill My Attribution?”
Fair question. Short answer: no, as long as your attribution is client-side — and it almost certainly is.
This is a rewrite, not a redirect. The visitor’s browser still shows (and keeps) the full original URL with every UTM intact. Cloudflare only cleans the copy of the URL that travels onward to the cache and your origin. And every mainstream tracker reads the URL in the browser:
- GA4 (gtag.js) reads
utm_*fromlocation.href. - Meta Pixel picks up
fbclidclient-side. - Google Ads, TikTok, Klaviyo, Mailchimp — same story, JavaScript reading the browser URL.
- WooCommerce Order Attribution (core since 8.5) is client-side too: sourcebuster.js reads the URL in the browser, stores the source in cookies, and checkout reads the cookies.
There’s also a point most people miss: if you run full-page caching, your server already wasn’t seeing those parameters in any useful way. A cache hit never executes PHP. Any “server-side” attribution that relied on PHP reading the query string was already fiction for every cached visit. The Transform Rule doesn’t take something away — it makes explicit what was already true: on a cached site, attribution lives in the browser.
Where You Should Be Careful
Honest caveats, because there are a few:
- Affiliate and referral plugins that read params server-side. The list includes
ref— which is also the default referral parameter of some affiliate plugins (AffiliateWP, for one). If your site uses?ref=(or the very genericpp) for anything real, remove it from both expressions. The repo README flags exactly these two. - Server-side personalization. If a landing page changes its content in PHP based on
utm_campaign, it will stop seeing it. (On a cached page it already couldn’t do this reliably — another reason that pattern is a trap.) - Log-based campaign analytics. Your access logs get cleaner, which is the point — but if you mine logs for UTM data, that data ends here.
- Rule order. If you already have Transform or Redirect Rules touching the query string, check the execution order under Rules → Overview.
None of these apply to a typical WooCommerce store running GA4 plus a pixel or two. Review the parameter list against your own known query params once, then deploy.
Ten Minutes, One Rule
Grab the expressions from the repo, paste them into Rules → Create rule → Rewrite URL, deploy, and test with a ?utm_source=test link: github.com/mateusz-zadorozny/cfrules.
Think of a cinema where every guest bought a ticket through a different promo — newsletter, Facebook ad, Google. The usher tears off the promo stub at the door: each guest keeps their half, so marketing still knows exactly which promo sold which seat. But inside, everyone watches the same single screening. Without the usher, you’d be running a private screening per promo code — or worse, performing the film live, from scratch, for every “promo” guest.
Stripping tracking params is one small piece of a cache setup that actually holds under campaign traffic. If you’d rather have someone tune the whole stack — cache keys, edge rules, server-level caching — that’s the kind of thing I do in WooCommerce Care.