Crawler URL Normalization Without Losing Query Semantics
Separate request URLs from comparison keys, preserve meaningful parameters and test both URLs that should merge and those that must stay distinct.
KAIROXO

URL normalization can save a crawler from collecting the same resource through many spelling variations. It can also destroy data when a seemingly harmless transformation removes the parameter that selects a language, page or product option. Treat normalization as a documented equivalence rule, not a string-cleaning exercise. The question is whether two requests mean the same thing to the particular origin.
Imagine an approved documentation crawl discovering links from navigation, search results and campaign pages. Some links differ only by tracking information; others select translated manuals. We want to reduce duplicate work without combining distinct documents or modifying signed URLs that must arrive exactly as issued.
Separate request URLs from comparison keys
Keep the original discovered URL, the resolved request URL and the comparison key as separate concepts. The original explains where a link came from. The request URL is what the client actually fetches. The comparison key decides whether work is already known. A transformation useful for comparison does not automatically belong on the wire.
Cloudflare's normalization discussion illustrates how equivalent-looking paths affect rule matching. A crawler has a related identity problem, but it should not copy every edge-server transformation blindly. An origin may interpret repeated slashes, percent escapes or query ordering in a particular way. Verify the equivalence that your own collection relies upon.
Implementation steps
Resolve relative links with a real URL parser
Use the platform URL parser with the page's effective base URL. String concatenation fails on parent paths, query-only links and protocol-relative references. When the document includes a base element, decide whether your extractor respects it and validate the resulting destination. A successful parse only establishes syntax; it does not establish that the URL belongs to the allowed crawl.
Restrict schemes to those your collector supports, typically HTTP and HTTPS. Reject embedded user credentials rather than allowing a discovered link to introduce a new authentication context. Match the parsed hostname and port against your scope rules. Do not use substring checks that accept an unrelated hostname containing the permitted name.
Preserve meaningful query parameters
Create a per-origin list of parameters proven irrelevant to the required representation. Tracking parameters may be candidates, but there is no universal safe deletion rule. A parameter named source could select a data source, not a campaign. Keep language, pagination, filters and variant selectors unless you have evidence that they do not change the resource you need.
Query strings can contain repeated keys, empty values and a significant order. Signed links may validate their exact encoding. Sorting all parameters or replacing plus signs can invalidate those links or change server behavior. The example removes only explicitly listed parameter names and leaves other ordering untouched. Use the generated value as a comparison key, not a universal request rewriter.
Codejavascript export function comparisonKey(input, { base, ignoredParameters = [] } = {}) { const url = new URL(input, base); if (!['http:', 'https:'].includes(url.protocol)) throw new Error('unsupported protocol'); if (url.username || url.password) throw new Error('credential-bearing URL'); url.hash = ''; const ignored = new Set(ignoredParameters); const parts = url.search.slice(1).split('&'); const kept = parts.filter(part => { // Decode the name for comparison only; retain every remaining raw field. const name = new URLSearchParams(part).keys().next().value; return !ignored.has(name); }); if (kept.length !== parts.length) url.search = kept.length ? `?${kept.join('&')}` : ''; // Do not sort query parameters, lowercase paths, or decode reserved escapes. return url.href;}Avoid destructive path transformations
Hostnames have case-insensitive semantics, but paths often do not. Lowercasing the whole URL can merge two distinct manuals. Likewise, an encoded slash is not automatically interchangeable with a path separator. Let the standards-based parser perform its defined work; avoid a second blanket percent-decoding pass that changes reserved delimiters.
A fragment usually identifies a location inside a retrieved document and is not sent in the HTTP request. Removing it from a fetch key is often useful, but browser applications can interpret fragments as client-side routes. If the task depends on rendered fragment routes, use a different identity policy and test it explicitly rather than applying the plain-HTTP rule.
Bound redirect and canonical handling
Record the final URL after redirects and verify that it remains in scope. A redirect is a network instruction; a canonical link is publisher-provided metadata about preferred identity. They are not interchangeable. A canonical may be wrong, point outside the approved domain or describe a different localized version. Do not automatically replace every discovered page with its canonical target.
Set redirect limits and detect loops. When multiple discovered URLs reach the same final representation, preserve the mapping so audit records still explain their origins. If the final page differs in language or selected variant, keep that distinction. Deduplication should reduce redundant work while preserving the evidence needed to explain what was actually collected.
Test equivalence and non-equivalence together
Build a small test table from real URL shapes before crawling widely. Positive cases might include hostname case or a verified tracking parameter. Negative cases should include different language values, reordered repeated parameters, uppercase paths, encoded slashes and signed URLs. A normalization test suite needs both kinds: proving only that duplicates collapse can hide over-aggressive merging.
Monitor rejected destinations and collision counts. An unexpected rise may indicate a new link format or an origin changing parameter semantics. Consult pagination completion rules when the parameters drive traversal. KAIROXO routing does not decide whether two source URLs describe the same document; that policy remains with the application.