Writing Nginx Regex Redirects for Query Strings
Problem Statement
You need Nginx redirects that match on or transform query-string parameters during a migration — for example mapping ?id=42 to a clean path, preserving utm_* trackers, or dropping a legacy parameter. Nginx does not match the query string inside a location regex; you have to use $args, $arg_<name>, or a map block. This page sits under Regex Redirect Rules.
When to Use This Approach
- Old URLs carry the page identity in a query parameter (
?id=,?p=,?page=). - You must preserve tracking parameters (
utm_source,gclid) across the redirect. - You need to strip a deprecated parameter while keeping the rest.
- A
location ~regex is silently ignoring the query string and you are not sure why. - You want a maintainable parameter-to-path map rather than dozens of
ifblocks.
Step-by-Step Instructions
1. Preserve the Whole Query String
A location regex matches only the path. To carry the query string onto the target, append $is_args$args — $is_args is ? when args exist and empty otherwise, so you never emit a dangling ?.
# Path rename that keeps every original query parameter
location ~* ^/old-category/(.*)$ {
return 301 /new-category/$1$is_args$args;
}
Preserving the whole query string is the default you want for almost every rule, and the idiom is short enough that omitting it is always an oversight rather than a decision. Append $is_args$args to the destination and a request with no parameters produces a clean URL while one carrying utm_source arrives with it intact. The failure when this is missing is entirely invisible to status-code testing, since the redirect works perfectly and simply discards the attribution.
2. Redirect Based on a Single Parameter
Nginx exposes each parameter as $arg_<name>. Match on it with a map (cleaner than nested if), then redirect only when it is present.
# Map a legacy ?id= value to a clean path
map $arg_id $id_target {
default "";
42 /products/dns-monitor;
43 /products/ttl-tuner;
}
server {
location = /product.php {
if ($id_target) { return 301 $id_target; }
}
}
Matching on one parameter is where positional patterns fail, because query-string order is not guaranteed by anything. A regex expecting ?utm_source=x&utm_medium=y matches requests constructed in that order and misses the identical request built the other way round — and both arrive in real traffic, because different sources assemble URLs differently. $arg_name reads the value regardless of position, which turns a rule that works most of the time into one that works.
3. Strip a Deprecated Parameter, Keep the Rest
To drop one parameter you must rebuild $args without it. Use a map over $args for the common cases, or normalise the full query string to a canonical form.
# Remove a legacy sessionid param while preserving other args
if ($args ~ (.*)(?:^|&)sessionid=[^&]*(.*)) {
set $clean_args $1$2;
return 301 $uri?$clean_args;
}
Stripping one parameter while keeping the rest means rebuilding the query rather than passing it through, and rebuilding is where parameters get lost. Enumerate what your application actually consumes, preserve those explicitly, and be conservative about what you drop — the parameters most often discarded by accident are the ones belonging to somebody else, such as an affiliate identifier or a partner’s tracking token, and their absence is reported to you by the partner rather than by any monitoring.
4. Collapse a Parameterised Variant to a Canonical URL
When several parameter combinations should resolve to one canonical page, map them all to the same target and drop the query string entirely.
# Old paginated/sorted variants all canonicalise to the clean category page
map $arg_sort $sort_seen { default 0; "" 0; }
location = /category {
if ($arg_sort) { return 301 /category; } # drop ?sort=, keep one canonical URL
}
Collapsing a parameterised variant to a canonical URL is the one case where discarding the query is correct — and it is worth being explicit that this is a deliberate exception rather than the default. Do it only where the parameter genuinely selected nothing that the destination cannot represent, and confirm against real requests that the parameters you are dropping are not consumed by the application or by a partner integration.
Worked Example
Legacy product pages used ?id=. The goal is a clean path that still preserves campaign trackers.
Before:
$ curl -sI 'https://www.example.com/product.php?id=42&utm_source=newsletter'
HTTP/2 200
With a map that captures id and re-appends remaining args, the request resolves to the clean path while keeping utm_source:
$ curl -sIL 'https://www.example.com/product.php?id=42&utm_source=newsletter' \
| grep -iE '^HTTP|^location'
HTTP/2 301
location: https://www.example.com/products/dns-monitor?utm_source=newsletter
HTTP/2 200
The page identity moves from query string to path in a single hop, and the campaign parameter survives.
Matching on a single parameter needs $arg_name rather than a regex over the whole query string, because parameter order is not guaranteed and a positional pattern will match some requests and not others depending on how the URL was constructed. $arg_utm_source gives you the value directly regardless of position, which turns a fragile pattern into a simple comparison.
Be deliberate about which parameters are significant. Stripping a deprecated parameter while preserving the rest requires reconstructing the query rather than passing it through, and the reconstruction is where parameters get lost — usually the ones nobody tested with, such as an affiliate identifier or a locale hint. Enumerate the parameters your application actually consumes, preserve those explicitly, and drop the remainder only where you have confirmed nothing depends on them.
Prefer a map block to if for anything with more than a couple of conditions. Nginx evaluates map at request time as a simple lookup, it keeps the logic declarative and reviewable, and it avoids the well-documented surprises around if inside a location context. A generated map populated from the mapping inventory is also the natural way to scale this pattern past a handful of rules, since lookup cost stays flat as the map grows.
Verification
- Confirm the parameter-driven redirect and final status:
curl -sIL 'https://www.example.com/product.php?id=42' | grep -iE '^HTTP|^location'. - Test tracker preservation by requesting a URL with
utm_sourceand confirming it appears on theLocation. - Test the strip rule by sending the deprecated parameter and confirming it is absent from the target while other args remain.
FAQ
Why does my location regex ignore the query string?
Because Nginx location matching only sees the normalised path — the query string is never part of the regex. Match parameters with $arg_<name> or a map over $args instead, and append $is_args$args to carry them onto the target.
Should I use map or if for query-string redirects?
Prefer map — it is evaluated once, is faster, and avoids the well-known pitfalls of if inside location. Reserve if for the narrow case of conditionally rebuilding $args, as in the strip example.
Should query-string variants be redirected or canonicalised? Canonicalised where they represent the same content and redirected only where the parameter genuinely selected different content. A sort order, a view preference or a tracking parameter does not change what the page is, so the right treatment is a canonical tag pointing at the clean URL — redirecting those breaks functionality for no benefit. A parameter that selected a distinct product variant or a distinct page of results is a different URL and should redirect to its new form.
Does location match against the query string?
No — location blocks match the path only, which is the single most common surprise in this area. Any decision that depends on a parameter has to be made with an if on $arg_name inside the matched location, or through a map block evaluated at request time. The map approach is preferable for anything beyond one or two conditions, since it keeps the logic declarative and avoids the well-known pitfalls of if in Nginx configuration.
Why does my redirect drop the query string when the path is correct?
Because Nginx does not carry it across unless the destination says so. A return 301 /new-path produces exactly that URL, parameters discarded, and the response is a perfectly valid redirect to a working page — which is why nothing flags it. Append $is_args$args and the original query is preserved when present and omitted cleanly when absent. This single omission is the most common cause of campaign attribution disappearing after a migration.
Should the canonical form strip parameters entirely? Only the ones that do not select content. Tracking parameters should reach the page so analytics can read them, then be excluded from the canonical tag rather than from the URL. Parameters that genuinely change what is shown — a variant, a page number, a filter that alters the result set — are part of the URL’s identity and stripping them redirects visitors to something other than what they asked for. The decision is per parameter class, made once and applied consistently.
Related
← Back to Regex Redirect Rules