TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

Build a TypeScript URL-Cleaning Extension with Manifest V3

Practical insights, clear explanations, and useful ideas for understanding the web.

October 26, 20267 min read5 topics
Build a TypeScript URL-Cleaning Extension with Manifest V3
Explore the details
#Chrome Extensions#TypeScript#Manifest V3#JavaScript#Web Development
October 26, 20267 min read

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 activeTab permission is a narrower choice than requesting access to every website.
  • Parse URLs with the browser’s URL API and allow only http: and https: URLs before changing the tab.
  • Chrome loads JavaScript, not TypeScript: compile popup.ts to popup.js before 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 include popup.js.
  • Requesting broad access unnecessarily: This example uses activeTab rather 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 URL and URLSearchParams APIs 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.