feat: add Clean URL Tracking Params userscript

Strips fbclid/gclid/utm_*/msclkid/ttclid and other known ad-tracking
query params from the URL bar via history.replaceState -- no reload,
no network requests. Handles SPA navigation too (pushState/replaceState
wrapping, popstate/hashchange), since client-side routers often re-add
tracking params on route changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 05:20:39 +02:00
co-authored by Claude Sonnet 5
parent 98d8a8a76d
commit c642f7e9a3
3 changed files with 163 additions and 0 deletions
+4
View File
@@ -12,6 +12,10 @@ unless a script's own README explicitly says otherwise.
page's live DOM and shows them on-screen as selectable text. Inspired
by [uidrop.site](https://uidrop.site)'s extraction categories, without
its AI hand-off — nothing is ever sent anywhere.
- [**Clean URL Tracking Params**](clean-url-tracking/) — strips known
ad/tracking query parameters (`fbclid`, `gclid`, `utm_*`, `msclkid`,
`ttclid`, and more) from the URL bar on load and on SPA navigation,
via `history.replaceState` — no reload, no network requests.
## Installing a script
+25
View File
@@ -0,0 +1,25 @@
# Clean URL Tracking Params
Strips known ad/tracking query parameters (`fbclid`, `gclid`, `utm_*`,
`msclkid`, `ttclid`, and more — see the full list in the script) from the
URL bar on page load and on single-page-app navigation, using
`history.replaceState`. The page never reloads and nothing is ever sent
anywhere — this only rewrites the URL already sitting in your own
address bar.
Only well-known ad-network/analytics-specific parameter names are
stripped. Generic-sounding params like `ref`, `source`, or `id` are
deliberately left alone, since real sites often depend on those for
actual navigation — this script only touches names that are
unambiguously tracking-only.
**Scope note**: this is a cosmetic/privacy URL cleanup, not a
network-level tracker blocker. If a tracking pixel already read the
param before this script ran, cleaning the URL afterward doesn't
un-fire it. Pair with a real request-blocking extension (uBlock Origin,
etc.) if that's the goal — this script just keeps the URL itself clean
to look at, copy, and share.
## Installing
See the [repo README](../README.md#installing-a-script).
@@ -0,0 +1,134 @@
// ==UserScript==
// @name Clean URL Tracking Params
// @namespace https://devops.cloudhost.es/Malin/userscripts
// @version 1.0.0
// @description Strips known ad/tracking query parameters (fbclid, gclid, utm_*, msclkid, ttclid, etc.) from the URL bar on load and on SPA navigation, without reloading the page. No network requests of any kind.
// @author Malin
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
/*
* Clean URL Tracking Params
* --------------------------
* Removes known tracking/analytics query parameters from the current
* page's URL via history.replaceState -- the page never reloads, nothing
* is sent anywhere, this only rewrites what's already in your own
* address bar. Runs once at document-start (before most page scripts
* get a chance to read the dirty URL) and again on every SPA navigation
* (history.pushState/replaceState calls, popstate, hashchange), since
* single-page apps often re-add tracking params on client-side route
* changes without a real page load.
*
* This is a cosmetic/privacy URL-bar cleanup, not a network-level
* tracker blocker -- a tracking pixel that already fired before this
* script ran (e.g. from a query param read by inline page JS) isn't
* un-fired by cleaning the URL afterward. Pair with an actual
* request-blocking extension (uBlock Origin etc.) if that's the goal;
* this script only keeps the URL itself clean to look at, copy, and share.
*/
(function () {
'use strict';
// Case-sensitive tracking param names to strip. Deliberately excludes
// generic-sounding params like "ref"/"source"/"id" that legitimate
// sites often depend on for real navigation -- only well-known
// ad-network/analytics-specific param names are listed here, to avoid
// ever breaking a page's actual functionality.
const TRACKING_PARAMS = new Set([
// Google Ads / Analytics
'gclid', 'gclsrc', 'dclid', 'wbraid', 'gbraid', '_gl',
'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
'utm_id', 'utm_source_platform', 'utm_creative_format', 'utm_marketing_tactic',
// Meta / Facebook / Instagram
'fbclid', 'fb_action_ids', 'fb_action_types', 'fb_ref', 'fb_source',
'igshid', 'igsh',
// Microsoft Ads
'msclkid',
// Twitter / X
'twclid',
// TikTok
'ttclid',
// Yahoo / Yandex
'yclid', '_openstat',
// Pinterest
'epik',
// Snapchat
'sc_cid',
// Mailchimp
'mc_eid', 'mc_cid',
// HubSpot
'_hsenc', '_hsmi', 'hsCtaTracking',
// Marketo
'mkt_tok',
// Vero
'vero_id', 'vero_conv',
// Piwik / Matomo
'pk_campaign', 'pk_kwd', 'pk_source', 'pk_medium', 'pk_content',
// Omeda
'oly_enc_id', 'oly_anon_id',
// Generic share-tracking token used by YouTube, Spotify, and others
'si',
]);
function cleanUrl(rawUrl) {
let url;
try {
url = new URL(rawUrl, location.href);
} catch (e) {
return null; // not a parseable URL -- leave it alone
}
if (!url.search) return null;
let changed = false;
for (const key of [...url.searchParams.keys()]) {
if (TRACKING_PARAMS.has(key)) {
url.searchParams.delete(key);
changed = true;
}
}
if (!changed) return null;
// Drop a bare trailing "?" left over once every param is gone.
let clean = url.pathname + (url.search ? url.search : '') + url.hash;
if (url.origin !== location.origin) {
clean = url.origin + clean;
}
return clean;
}
function cleanCurrentUrl() {
const clean = cleanUrl(location.href);
if (clean !== null) {
history.replaceState(history.state, '', clean);
}
}
// Initial clean -- as early as document-start allows. Some params
// live in location.href before any DOM exists yet, so this is safe
// to run immediately.
cleanCurrentUrl();
// Re-run once the document is actually interactive too, in case a
// very-early redirect changed the URL again before we got here.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', cleanCurrentUrl, { once: true });
}
// SPA navigation: history.pushState/replaceState are the standard way
// client-side routers change the URL without a real page load. Wrap
// both so a newly-set URL gets cleaned right after the app sets it,
// not fought over (we clean AFTER the app's own call completes, so we
// always see the final URL it intended).
for (const method of ['pushState', 'replaceState']) {
const original = history[method];
history[method] = function (...args) {
const result = original.apply(this, args);
cleanCurrentUrl();
return result;
};
}
window.addEventListener('popstate', cleanCurrentUrl);
window.addEventListener('hashchange', cleanCurrentUrl);
})();