Preserving Query Parameters Through Redirects

A redirect that silently drops the query string breaks campaign attribution; one that blindly preserves it creates an indexable duplicate for every tracking variant. The rule has to be explicit.

When to Use This Approach

Work through this when:

  • Paid or email traffic landing on redirected URLs shows as direct or organic in analytics.
  • A migration introduced pattern-based redirects that construct new destination URLs.
  • Tracking parameters are appearing on indexed URLs after a redirect.

Two Failure Modes, Opposite Directions

Dropping and preserving are both wrong for some parameter classes, so a single global policy cannot be correct.

Drop everything, keep everything, or select Three redirect policies compared on two outcomes — campaign attribution and index cleanliness — with only the selective policy achieving both. policy attribution index cleanliness drop everything broken clean keep everything intact polluted select by class intact clean Neither global policy is correct; the class of parameter decides Index cleanliness comes from the destination's canonical, not from dropping the parameter

Implementation Steps

Step 1: Audit what your redirects currently do

#!/usr/bin/env bash
# Test each rule with a representative query string
while IFS=$'\t' read -r from to; do
  eff=$(curl -s -o /dev/null -w '%{redirect_url}' \
    "https://example.com${from}?utm_source=test&utm_campaign=audit")
  case "$eff" in
    *utm_source=test*) verdict="preserved" ;;
    "")                verdict="no redirect" ;;
    *)                 verdict="DROPPED" ;;
  esac
  printf '%-40s %-12s %s\n' "$from" "$verdict" "$eff"
done < redirect-map.tsv

Validation: Group the output by rule type. Pattern-based rules that build a new destination almost always show DROPPED, while simple one-to-one rules usually preserve.

Step 2: Classify parameters by whether they must survive

// lib/redirect-params.js
export const SURVIVES_REDIRECT = new Set([
  // attribution — must reach the destination or reporting breaks
  'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
  'gclid', 'fbclid', 'msclkid', 'ref', 'affiliate',
  // functional state the destination needs
  'lang', 'currency',
]);

export const NEVER_SURVIVES = new Set([
  // the destination defines its own content and ordering
  'sort', 'order', 'page', 'view',
  // legacy parameters from the old URL structure
  'id', 'p', 'cat', 'sess',
]);

SEO impact: Legacy parameters from the pre-migration structure are the ones that create duplicates at the new URL, and they are exactly the ones a naive “preserve everything” rule keeps.

Validation: Any parameter in neither set should be logged and classified deliberately, not silently defaulted.

Step 3: Implement one merge function

// lib/merge-query.js
import { SURVIVES_REDIRECT } from './redirect-params';

export function mergeQuery(incomingSearch, destinationUrl) {
  const dest = new URL(destinationUrl);
  const incoming = new URLSearchParams(incomingSearch);

  for (const [key, value] of incoming) {
    if (!SURVIVES_REDIRECT.has(key)) continue;
    if (dest.searchParams.has(key)) continue;      // destination wins on conflict
    dest.searchParams.append(key, value);
  }

  dest.searchParams.sort();                        // deterministic ordering
  return dest.toString();
}

SEO impact: Sorting makes two equivalent redirects produce byte-identical destinations, which prevents the redirect itself from generating URL variants.

Validation: Assert that permuting the incoming parameter order yields an identical destination string.

Step 4: Apply it inside the redirect, not after it

The tempting mistake is a second redirect that strips parameters, which turns every campaign click into a two-hop journey.

// middleware.js
import { mergeQuery } from '@/lib/merge-query';
import { lookupRedirect } from '@/lib/redirect-map';

export function middleware(request) {
  const url = new URL(request.url);
  const target = lookupRedirect(url.pathname);       // final destination, already flattened
  if (!target) return NextResponse.next();

  const destination = mergeQuery(url.search, new URL(target, url.origin).toString());
  return NextResponse.redirect(destination, 301);
}

SEO impact: One hop preserves the maximum signal and the minimum latency. The chain-flattening discipline is covered in flattening redirect chains in edge middleware.

Validation: curl -s -o /dev/null -w '%{num_redirects}\n' -L 'https://example.com/old-path?utm_source=news' returns 1.

Step 5: Verify attribution and indexation separately

They are different tests and passing one says nothing about the other.

# Attribution: parameters arrive at the destination
curl -s -o /dev/null -w '%{url_effective}\n' -L \
  'https://example.com/old-product?utm_source=email&sort=price&id=4471'
# Expect: /new-product/?utm_source=email   (sort and id dropped)

# Indexation: the destination canonicalises the survivors away
curl -s 'https://example.com/new-product/?utm_source=email' \
  | grep -oP '(?<=rel="canonical" href=")[^"]+'
# Expect: https://example.com/new-product/   (no parameters)

Validation: Both must hold. Attribution without canonicalisation puts campaign URLs in the index; canonicalisation without attribution loses the reporting.

The Full Path of a Campaign Click

Following one click end to end shows why the two tests are separate and where each parameter is dropped.

Where each parameter is dropped along the click path A campaign URL with four parameters passing through an edge redirect that keeps only attribution, arriving at a destination that records them in analytics and canonicalises to a clean URL. /old-product?utm_source=email&utm_campaign=spring&sort=price&id=4471 edge redirect — one hop keeps attribution, drops sort and id /new-product/?utm_campaign=spring&utm_source=email analytics records the campaign attribution intact canonical: /new-product/ index stays clean

SEO Impact Summary

Signal What improves What breaks if misconfigured
Redirect efficiency One hop from campaign URL to destination, with no cleanup redirect A second redirect to strip parameters doubles the latency and dilutes the signal
Index composition Tracking variants are canonicalised away at the destination Preserved legacy parameters create indexable duplicates of the new URL
Attribution accuracy Paid and email traffic is credited correctly after a migration Dropped parameters reclassify campaign traffic as direct
Migration safety Legacy parameters from the old structure do not follow content to its new home Old identifiers arriving at new URLs can trigger unintended behaviour

Measurable signals to watch:

  • Redirect hop count for campaign URLs, asserted at one in CI.
  • Share of paid sessions reported as direct, which should fall after the fix.
  • Indexed URL count containing utm_, which should stay at zero.

Edge Cases and Gotchas

Parameter handling and chain flattening interact: a cleanup redirect added to strip parameters converts every campaign click into two hops.

Cleanup redirect compared with in-rule parameter handling A campaign URL taking two hops when a cleanup redirect strips parameters, against a single hop when the merge happens inside the original rule. two hops legacy URL cleanup redirect destination one hop legacy URL destination merge happens inside the rule Parameter policy belongs in the redirect, never in a redirect after it

A parameter can be attribution in one place and content in another ref is a referral tag on marketing URLs and a reference identifier in some catalogues. Classify per path prefix if the collision is real, rather than picking one global answer that is wrong half the time.

Fragment identifiers never reach the server Anything after the hash is client-side only, so a redirect cannot preserve or drop it — the browser reapplies it to the destination. Do not attempt to handle it at the edge.

Encoded parameters must not be double-encoded Building the destination with URLSearchParams and then string-concatenating another query string is the usual cause. Construct once, serialise once.

Redirect maps generated from a spreadsheet often include query strings in the source path A rule keyed on /old?id=1 will not match /old?id=1&utm_source=x. Match on path, then handle parameters in the merge function, or the rule silently misses real traffic.

Frequently Asked Questions

Do query parameters survive a 301 automatically? It depends on the layer. Most CDN and framework implementations append the original query string by default, but pattern-based rules that construct a new URL usually drop it. Test each rule with a query string — the two behaviours look identical in configuration.

Should tracking parameters survive a redirect? Yes. Dropping them breaks attribution for every paid and email click landing on a redirected URL. They are safe to preserve provided the destination canonicalises them away, so they reach analytics without entering the index.

What if the destination already has its own parameters? The destination’s win on conflict, because they are part of the target you chose. Merge rather than replace: keep the destination’s values, add the allowed incoming ones that do not collide, drop the rest.

Does preserving parameters affect how much signal a 301 passes? Not materially. What matters far more is that the redirect resolves in a single hop to a 200 — chains and loops cost signal, while a query string on the destination does not, provided the canonical resolves it. The auditing method is in auditing redirect hops after a headless migration.


Part of: Redirect Chain Management

Related