Build a small Chrome extension that removes selected tracking parameters from the active tab’s URL. This tutorial uses TypeScript and Manifest V3, requests access only after the user opens the extension, and shows how to test and package the result.
Key Takeaways
- A popup-only extension can clean the active tab without a background service worker.
- The
activeTabpermission is a narrower choice than requesting access to every website. - Parse URLs with the browser’s
URLAPI and allow onlyhttp:andhttps:URLs before changing the tab. - Chrome loads JavaScript, not TypeScript: compile
popup.tstopopup.jsbefore testing or packaging.
1. Create the project
You’ll need Node.js and Chrome. Make a project directory and install TypeScript and Chrome’s API type definitions:
mkdir tidy-url-extension
cd tidy-url-extension
npm init -y
npm install --save-dev typescript @types/chrome
Create a tsconfig.json in the project root:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"types": ["chrome"],
"strict": true,
"noEmitOnError": true
},
"include": ["popup.ts"]
}
This configuration emits popup.js beside popup.ts. The extension will load the generated JavaScript file; it does not run TypeScript in the browser.
2. Add a least-privilege Manifest V3 manifest
Create manifest.json:
{
"manifest_version": 3,
"name": "Tidy URL",
"version": "1.0.0",
"description": "Remove selected tracking parameters from the active tab URL.",
"permissions": ["activeTab"],
"action": {
"default_popup": "popup.html"
}
}
The popup is opened when the user clicks the extension’s toolbar button. activeTab provides temporary access to the active tab in response to that user action. This example does not request broad host permissions or the tabs permission, and it does not need a service worker.
Permissions should match the extension’s actual behavior. If you later add features that access sites without a user action, reassess the required permissions and explain them to users; don’t add permissions preemptively.
3. Build the popup
Create popup.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tidy URL</title>
</head>
<body>
<main>
<h1>Tidy URL</h1>
<button id="clean" type="button">Clean this URL</button>
<p id="status" role="status" aria-live="polite">Ready.</p>
</main>
<script src="popup.js" defer></script>
</body>
</html>
The HTML loads a local script rather than embedding JavaScript inline. The status element provides feedback after the user clicks the button.
4. Parse and clean URLs safely
Create popup.ts:
const cleanButton = document.querySelector<HTMLButtonElement>("#clean");
const status = document.querySelector<HTMLParagraphElement>("#status");
if (!cleanButton || !status) {
throw new Error("The popup is missing a required element.");
}
const trackingParameters = new Set([
"fbclid",
"gclid",
"dclid",
"msclkid"
]);
function cleanUrl(rawUrl: string): URL | null {
let url: URL;
try {
url = new URL(rawUrl);
} catch {
return null;
}
if (url.protocol !== "http:" && url.protocol !== "https:") {
return null;
}
for (const key of [...url.searchParams.keys()]) {
const normalizedKey = key.toLowerCase();
if (normalizedKey.startsWith("utm_") || trackingParameters.has(normalizedKey)) {
url.searchParams.delete(key);
}
}
return url;
}
cleanButton.addEventListener("click", () => {
cleanButton.disabled = true;
status.textContent = "Checking the active tab…";
chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => {
const queryError = chrome.runtime.lastError;
const tab = tabs[0];
if (queryError || tab?.id === undefined || !tab.url) {
status.textContent = "Could not read the active tab URL.";
cleanButton.disabled = false;
return;
}
const cleaned = cleanUrl(tab.url);
if (!cleaned) {
status.textContent = "This page does not have a supported web URL.";
cleanButton.disabled = false;
return;
}
if (cleaned.href === tab.url) {
status.textContent = "No selected tracking parameters found.";
cleanButton.disabled = false;
return;
}
chrome.tabs.update(tab.id, { url: cleaned.href }, () => {
const updateError = chrome.runtime.lastError;
status.textContent = updateError
? "Could not update the tab. Try again."
: "Cleaned URL opened in this tab.";
cleanButton.disabled = false;
});
});
});
The cleaner removes utm_ parameters and a short, explicit list of common click identifiers. It preserves other query parameters and the URL fragment. The matching is case-insensitive for parameter names, while deletion uses the original key. If the same key occurs more than once, URLSearchParams.delete() removes all of its occurrences.
Using new URL() avoids treating arbitrary text as a web address. The protocol check also prevents this extension from attempting to navigate to schemes such as javascript: or file:. The code changes the URL in the active tab; it does not fetch the destination or validate the safety of a website.
5. Compile and test locally
Compile the TypeScript:
npx tsc
If there are no compiler errors, TypeScript generates popup.js. In Chrome, open chrome://extensions, turn on Developer mode, choose Load unpacked, and select the project directory. Open a test page such as:
https://example.com/page?utm_source=newsletter&ref=partner#details
Click the extension button, then Clean this URL. The tab should navigate to:
https://example.com/page?ref=partner#details
After editing a file, run npx tsc again. Use Reload for the extension on chrome://extensions, then reopen the popup. If Chrome reports a manifest or script error, check that manifest.json, popup.html, and the generated popup.js are all in the selected directory and that the script filename matches the manifest’s popup page setup.
6. Do you need a service worker?
Not for this example: all work happens in the popup in response to a click. A Manifest V3 service worker is useful for extension events that must be handled outside an open popup, but it has no DOM and should not be treated like a permanently running background page. Chrome can stop an extension service worker when it is idle, so important state should not depend on a global variable surviving between events. Register event listeners at the top level and persist state with an appropriate storage API when it must outlast a worker run.
If you add a worker, declare it under the manifest’s background key and compile its TypeScript to JavaScript as well. Keep the worker focused on event-driven tasks; the popup remains the right place for interface elements.
7. Package the extension
Before packaging, compile the latest TypeScript and check the project directory contains at least:
manifest.json
popup.html
popup.js
You can keep popup.ts, tsconfig.json, and package files in your development directory, but they are not needed for the extension to run. For distribution, make an archive containing the manifest and the runtime files (plus any required assets), not node_modules. Follow Chrome Web Store’s current submission instructions if you plan to publish there; loading an unpacked extension is only a local development workflow.
For a quick manual comparison of a URL before and after editing, TidyURLs also has a URL cleaner.
Common Mistakes
- Forgetting to compile: Chrome cannot load
popup.ts; compile it and includepopup.js. - Requesting broad access unnecessarily: This example uses
activeTabrather than permanent access to all sites. - Removing every query parameter: Some parameters are needed for search, pagination, or application behavior. Remove only parameters your extension explicitly intends to target.
- Assuming every tab has a normal web URL: New-tab pages and browser-internal pages may not expose a usable URL or support navigation through this flow. Handle missing URLs and unsupported schemes.
- Treating URL parsing as site validation: Parsing ensures a value is structurally a URL; it does not establish that the destination is trustworthy.
- Expecting a service worker to stay alive: Persist state that must survive worker shutdown rather than relying on in-memory globals.
Best Practices
- Keep the removal list narrow and document what it strips.
- Use the
URLandURLSearchParamsAPIs instead of concatenating URL strings. - Give users visible feedback for unchanged URLs and failures.
- Re-test after changing the manifest or generated files.
- Request permissions only when a feature needs them, and explain the reason.
Conclusion
A small Manifest V3 URL-cleaning extension needs only a popup, a focused manifest, and compiled TypeScript. Using activeTab, explicit URL-scheme checks, and a limited parameter list keeps the example understandable and avoids requesting more access than its core feature requires.
References
Live documentation retrieval was not available while preparing this article, so the links below are provided as official Chrome documentation to check against the Chrome version you target; they were not live-verified for this draft.
