urlpurify
v2.2.1
Published
Unwrap redirect, affiliate, and tracking wrapper URLs and strip tracking parameters. Turn noisy links into clean, direct URLs.
Maintainers
Readme
urlpurify
Unwrap redirect, affiliate, and tracking wrapper URLs and strip tracking parameters. Turn noisy links into clean, direct URLs.
Links from feeds, emails and social platforms rarely point straight at their destination. They pass through search redirects, link shims, email gateways and affiliate networks, and pick up tracking parameters on the way, the link decoration that follows a reader from site to site. urlpurify unwraps 380+ known wrappers and strips 100+ known tracking parameters plus vendor families. Campaign tags like utm_* and affiliate ids stay by default: they serve the site, not a tracker. It has no dependencies and runs in any modern JavaScript runtime, browsers included.
Installation
npm install urlpurifyQuick start
import { cleanUrl } from 'urlpurify'
const url = 'https://www.google.com/url?q=https%3A%2F%2Fexample.com%2Fpost%3Ffbclid%3Dabc'
cleanUrl(url) // => 'https://example.com/post'API
cleanUrl(url, options?)
Unwraps wrappers, repeatedly since they nest, then strips tracking parameters. The result is serialized per the URL Standard, and input that can't be parsed comes back unchanged, so the result is always safe to display.
import { cleanUrl, defaultTrackingParams, defaultUnwrappers, unwrapWebArchive } from 'urlpurify'
cleanUrl(url, {
// First match wins per pass.
unwrappers: [...defaultUnwrappers, unwrapWebArchive],
// Names, or regexes tested on the lowercased name.
trackingParams: [...defaultTrackingParams, 'newsletter_id', /^cmp_[a-z0-9_]+$/],
// The default.
maxUnwrapDepth: 12,
})An unwrapped target that isn't a usable URL is ignored, and the wrapper is kept.
unwrapUrl(url, unwrappers?)
Runs one pass of the unwrappers and returns the first target, or undefined.
stripTrackingParams(url, trackingParams?)
Removes tracking parameters and returns the serialized URL. Both this and cleanUrl apply three rules on top of the list:
refis dropped when it names the URL's own host, as in Ghost's?ref=example.com, and kept otherwise.- A query with a signature (
sig,signature,X-Amz-SignatureorX-Goog-Signature) stays whole, since dropping anything breaks the signature. tsstays on Alibaba DirectMail click URLs, which fail without it.
A tracking list is compiled on first use and cached per array. To change it, pass a new array.
createParamExtractor(config)
Builds an unwrapper for a target that sits in a query parameter:
import { cleanUrl, createParamExtractor, defaultUnwrappers } from 'urlpurify'
const unwrapExample = createParamExtractor({
// The domain and every subdomain. Use `hosts` for exact hosts or a regex.
domains: 'example.com',
path: '/out',
params: ['target'],
})
cleanUrl(url, { unwrappers: [...defaultUnwrappers, unwrapExample] })A value still encoded after one decode, starting with http%3A or https%3A, is decoded once more. A malformed escape, or one in a legacy charset such as %E9 for é in Latin-1, stays as written. For any other shape, write a plain UrlUnwrapper: a function that takes a URL and returns the target or undefined.
Tracking parameters
Tracking parameters are grouped like the unwrappers, by what stripping them takes away from someone:
| Category | Export | Stripping removes | Default |
| --- | --- | --- | :---: |
| Click attribution | clickAttributionParams | Ad, search and platform click ids | ☑️ |
| Visitor ids | visitorIdParams | Ids of one visitor, session, browser or email recipient | ☑️ |
| Housekeeping | housekeepingParams | Nothing: cache busters, previews and redirect leftovers | ☑️ |
| Campaign tags | campaignParams | The site's own traffic stats, from tags like utm_* | |
| Affiliate ids | affiliateParams | The publisher's commission | |
defaultTrackingParams combines the first three. A campaign tag carries the same value for every reader, so it tells the site where a visit came from and nothing about who made it. To strip more, spread them next to the defaults:
import { campaignParams, defaultTrackingParams, stripTrackingParams } from 'urlpurify'
stripTrackingParams(url, [...defaultTrackingParams, ...campaignParams])trackingParamsLiterals and trackingParamsPatterns hold every known parameter, opt-in categories included, split into names and regexes.
Unwrappers
There are more than 380 unwrappers, each in its own file in src/unwraps, with a comment naming the service and the URL shape it handles. Each is exported on its own and belongs to one category. The categories split by what unwrapping takes away from someone, which is what decides whether you want it:
| Category | Export | Unwrapping removes | Default |
| --- | --- | --- | :---: |
| Search clicks | searchClickUnwrappers | The search engine's or aggregator's click logging | ☑️ |
| Link shims | linkShimUnwrappers | Nothing, or a platform's click count | ☑️ |
| Press releases | pressReleaseUnwrappers | The issuer's click stats from the wire | |
| Sign-in shims | signInShimUnwrappers | Nothing, but the shim's page needs a sign-in | |
| Security gateways | securityGatewayUnwrappers | The gateway's click-time check of the target | |
| Email tracking | emailTrackingUnwrappers | The sender's click stats | |
| Affiliate links | affiliateUnwrappers | The publisher's commission | |
| Advertising | advertisingUnwrappers | Ad click and app install attribution | |
| Click counters | clickCounterUnwrappers | A site's or feed's count of which links get clicked | |
| Download measurement | downloadMeasurementUnwrappers | A podcast measurement prefix's download counts, or a site's download counts | |
| Archives and proxies | archiveProxyUnwrappers | The archived or translated copy: you get the live page | |
| Image proxies | imageProxyUnwrappers | The resized or optimized copy: you get the original image | |
| Embed proxies | embedProxyUnwrappers | The site's frame around another provider's embed: you get the provider's page | |
defaultUnwrappers combines the first two. The rest cost somebody something or change the page: a feed click counter tells a blog which posts get read, an affiliate link may pay for a small blog, and an archive link points at a copy on purpose. To turn more on, spread them next to the defaults:
import { affiliateUnwrappers, cleanUrl, defaultUnwrappers } from 'urlpurify'
cleanUrl(url, { unwrappers: [...defaultUnwrappers, ...affiliateUnwrappers] })