From c642f7e9a3260ddb8fdca79800696e7e807bd882 Mon Sep 17 00:00:00 2001 From: Malin Date: Tue, 18 Aug 2026 05:20:39 +0200 Subject: [PATCH] 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 --- README.md | 4 + clean-url-tracking/README.md | 25 ++++ clean-url-tracking/clean-url-tracking.user.js | 134 ++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 clean-url-tracking/README.md create mode 100644 clean-url-tracking/clean-url-tracking.user.js diff --git a/README.md b/README.md index e15dfe3..57e9dc4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/clean-url-tracking/README.md b/clean-url-tracking/README.md new file mode 100644 index 0000000..c19940e --- /dev/null +++ b/clean-url-tracking/README.md @@ -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). diff --git a/clean-url-tracking/clean-url-tracking.user.js b/clean-url-tracking/clean-url-tracking.user.js new file mode 100644 index 0000000..2f8d080 --- /dev/null +++ b/clean-url-tracking/clean-url-tracking.user.js @@ -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); +})();