/ โ† Redirect Fixes / Apache and nginx
In this guide
  1. Which directive for which job
  2. Apache: mod_alias, mod_rewrite and .htaccess
  3. nginx: return, rewrite and server blocks
  4. Regular expressions that do not overreach
  5. Query strings: kept, dropped or matched
  6. Long redirect lists without long config files
  7. The fix, step by step
  8. When a rule does not do what you expect
  9. Questions and answers

Which directive for which job

Both servers offer a simple directive and a powerful one. The simple one is easier to read, harder to get wrong and cheaper to run, so use it unless you need what only the powerful one offers.

JobApachenginx
One old path to one new pathRedirect 301 /old /new (mod_alias)location = /old { return 301 /new; }
A pattern of paths, such as a whole folderRedirectMatch 301 ^/old/(.*)$ /new/$1location ^~ /old/ { rewrite ^/old/(.*)$ /new/$1 permanent; }
Depends on host, scheme, header or query stringRewriteCond plus RewriteRule (mod_rewrite)A separate server block, or map plus return
Whole host to another hostA dedicated VirtualHost with Redirect 301 / https://www.example.co.uk/A dedicated server block with return 301 https://www.example.co.uk$request_uri;
Hundreds of one-to-one movesRewriteMap (main configuration only)map in the http block

Two behaviours trip people up. Apache's Redirect is a prefix match: Redirect 301 /shop /store also sends /shop/boots to /store/boots and /shopping to /storeping. If you mean one exact page, use RedirectMatch 301 ^/shop$ /store. And nginx's return stops processing immediately, while rewrite changes the URI and may continue through other rules unless you use permanent, redirect, last or break.

๐Ÿ’ก In nginx, a redirect that depends only on the host belongs in its own server block, not inside an if ($host = ...) test. Separate blocks are clearer, faster and avoid the well-known surprises of if inside location.

Apache: mod_alias, mod_rewrite and .htaccess

On Apache, rules can live in the main server configuration, a virtual host, or an .htaccess file in a folder. .htaccess is read on every request and only works if the virtual host allows it with AllowOverride; if nothing you write there has any effect, check that first. On shared hosting, .htaccess is usually the only place you can edit.

Things that behave differently in .htaccess

A clean mod_rewrite block

RewriteEngine On

# 1. host and scheme in one hop (skip if the CDN or panel already does this)
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\.example\.co\.uk$ [NC]
RewriteRule ^ https://www.example.co.uk%{REQUEST_URI} [R=301,L]

# 2. single moved pages
RewriteRule ^services/plumbing-repairs$ /services/plumbing/ [R=301,L]
RewriteRule ^team\.html$ /about/team/ [R=301,L]

# 3. whole folder moved, keeping the rest of the path
RewriteRule ^news/(.+)$ /blog/$1 [R=301,L]

# 4. application front controller last
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [END]

Order matters: host rules first, then specific pages, then patterns, then the catch-all that hands everything else to your application. If the catch-all comes first, none of the redirects below it ever run.

โš ๏ธ If the site sits behind a CDN or load balancer that terminates HTTPS, Apache sees every request as plain HTTP and %{HTTPS} off is always true, which causes a loop. Test %{HTTP:X-Forwarded-Proto} instead, and only if the proxy sets it reliably.

nginx: return, rewrite and server blocks

nginx has no per-folder files. Every rule lives in the configuration, which is checked and loaded when you reload the service. That makes rules faster and more predictable, but it means you need server access, or a hosting panel box that writes into the configuration for you.

# every http request, any host, goes straight to the final https host
server {
    listen 80;
    server_name example.co.uk www.example.co.uk;
    return 301 https://www.example.co.uk$request_uri;
}

# https on the bare domain
server {
    listen 443 ssl;
    server_name example.co.uk;
    # ssl_certificate lines here
    return 301 https://www.example.co.uk$request_uri;
}

server {
    listen 443 ssl;
    server_name www.example.co.uk;

    location = /services/plumbing-repairs { return 301 /services/plumbing/; }
    location = /team.html                 { return 301 /about/team/; }
    location ^~ /news/ {
        rewrite ^/news/(.+)$ /blog/$1 permanent;
    }

    location / {
        try_files $uri $uri/ /index.php?$args;
    }
}

How nginx chooses a location matters for redirects. Exact matches with = win outright. Otherwise nginx finds the longest matching prefix; if that prefix is marked ^~ it is used, and if not nginx tries regular expression locations in the order they appear. A redirect placed in a regex location can therefore be skipped because another regex location earlier in the file matched first. Exact and ^~ locations avoid the guesswork.

Use $request_uri when copying the whole original path and query string, because it is the address exactly as the visitor sent it. $uri is the decoded, normalised path without the query string, and can change after internal rewrites.

Regular expressions that do not overreach

Most broken redirects are pattern mistakes. These are the ones the Redirect Checker exposes most often:

PatternWhat goes wrongSafer version
old-pageNo anchors, so it also matches my-old-page-archive.^old-page$
^products/(.*) to /shop/$1Also matches products/ alone, redirecting the listing page to an empty /shop/ you may not want.^products/(.+)$
^blog/(.*)$ to /blog/$1/The target matches the pattern, so /blog/post/ redirects to /blog/post//, and so on: a loop.^blog/(.*[^/])$, or add a condition that the path does not already end in a slash.
^page.html$The unescaped dot matches any character, so pagexhtml matches too.^page\.html$
^About-Us$Case-sensitive; /about-us does not match.Add [NC] on Apache or use ~* in nginx, if the old URLs really varied in case.

The loop test is simple and worth doing for every pattern rule: write down the target for one real example, then ask whether that target would match the same pattern. If it would, the rule needs a tighter pattern or a condition that excludes the target.

Query strings: kept, dropped or matched

The two servers handle the part after ? differently, and the difference decides whether campaign tags survive a redirect.

# Apache: old product IDs in the query string
RewriteCond %{QUERY_STRING} (^|&)id=42(&|$)
RewriteRule ^product\.php$ /shop/oak-bench/? [R=301,L]

# nginx: same move
location = /product.php {
    if ($arg_id = "42") { return 301 /shop/oak-bench/; }
}

The trailing ? on the Apache target drops id=42 so it is not carried onto the new, clean address.

Long redirect lists without long config files

After a site rebuild you may have hundreds of old URLs. Hundreds of separate rules are slow to read and easy to break, and in .htaccess they are evaluated on every request. Both servers have a lookup table for this.

# nginx: in the http block
map $request_uri $moved_to {
    default                         "";
    /services/plumbing-repairs      /services/plumbing/;
    /team.html                      /about/team/;
    /news/opening-hours-2024        /blog/opening-hours/;
}

# in the server block
if ($moved_to) { return 301 $moved_to; }
# Apache: in the virtual host, not .htaccess
RewriteMap moved "txt:/etc/apache2/redirects/moved.txt"
RewriteCond ${moved:%{REQUEST_URI}|NONE} !=NONE
RewriteRule ^ ${moved:%{REQUEST_URI}} [R=301,L]

# moved.txt, one pair per line
/services/plumbing-repairs /services/plumbing/
/team.html /about/team/

Keep the list in a file under version control or in a spreadsheet you export, and before loading it, check that no target also appears as a source. That single check prevents most chains.

๐Ÿ’ก The nginx map above matches $request_uri, which includes the query string. If old links often carry tracking tags, match on $uri instead so /team.html?utm_source=x still finds its entry.

The fix, step by step

Step 1

Find every rule that already redirects

Search the server for redirect rules before adding any. On Apache, look in the virtual host files and every .htaccess from the web root downwards for Redirect, RedirectMatch and RewriteRule lines with an R flag. On nginx, run nginx -T and search the output for return 30 and rewrite. Note which file each rule is in and whether a CDN, panel or CMS also adds redirects.

Step 2

Pick the simplest directive for each job

Use an exact location with return 301 in nginx, or RedirectMatch with anchors in Apache, for single pages. Reserve RewriteRule and rewrite for rules that genuinely need conditions or captured parts of the old path. Put whole-host redirects in their own virtual host or server block. Replacing clever rules with plain ones makes the file shorter and removes most accidental matches before they ever reach a visitor.

Step 3

Anchor and escape every pattern

Start each pattern with ^ and end it with $, escape literal dots as \., and use (.+) rather than (.*) when the captured part must not be empty. In .htaccess, leave out the leading slash. For each rule, take one real example, work out its target, and confirm the target does not match the same pattern, which is how redirect loops are born.

Step 4

Decide what happens to the query string

For each rule, decide whether tracking tags, search terms and other parameters should follow the visitor to the new address. Apache RewriteRule keeps them unless the target ends in ? or you add [QSD]; nginx return drops them unless you add $is_args$args. Where old URLs identified pages by a parameter, match it with RewriteCond %{QUERY_STRING} or $arg_name and drop it from the target, so the new page has a clean address.

Step 5

Move long URL lists into a map

If you have more than a few dozen one-to-one redirects, move them into an nginx map or an Apache RewriteMap text file in the main configuration. Before loading the list, sort it and check that no destination appears anywhere in the source column, and that every destination returns 200. Keep the file under version control so later additions are reviewed rather than pasted in at the bottom.

Step 6

Test the syntax, then trace each rule

Run apachectl configtest or nginx -t before every reload so a typo cannot take the site down. Then request each changed URL with curl -sI and follow the chain with the Redirect Checker from the http and non-www starting points as well. Every old address should reach a 200 page in one hop, with the query string handled the way you decided.

When a rule does not do what you expect

Both servers can tell you exactly what their rewrite engine did, which is quicker than guessing.

curl -sI http://example.co.uk/news/spring-opening | grep -iE "^(HTTP|location|server)"
Questions and answers

1. Should I use Redirect or RewriteRule in .htaccess?

Use Redirect or RedirectMatch for straightforward page and folder moves, because they are easier to read and have fewer surprises. Remember that Redirect is a prefix match, so it catches everything that starts with the old path; RedirectMatch with anchors is more precise. Use RewriteRule when the redirect depends on something other than the path, such as the host, the scheme, a header or the query string. Avoid mixing both modules for the same URLs in one file.

2. Why does my RewriteRule work in the virtual host but not in .htaccess?

In .htaccess the path that RewriteRule sees has the folder prefix removed, including the leading slash. A pattern written as ^/old-page$ matches in a virtual host but never matches in .htaccess, where it needs to be ^old-page$. Also check that the virtual host has AllowOverride set to allow rewrite rules for that folder, and that RewriteEngine On appears in the file. Without a suitable AllowOverride setting, Apache ignores the .htaccess file completely and nothing in it runs.

3. Is nginx return 301 better than rewrite permanent?

For a fixed destination, yes. return sends the redirect immediately without evaluating a regular expression, and it cannot accidentally continue into other rules. rewrite with permanent is useful when you need to capture part of the path and reuse it in the target, such as moving a whole folder. Both send a 301. A common pattern is an exact or prefix location for the old path containing a single return or rewrite.

4. Does a redirect keep the query string?

It depends on the directive. Apache RewriteRule keeps the original query string unless the target ends with a question mark or you add the QSD flag, and it replaces it if the target has its own query string unless you add QSA. nginx rewrite appends the original arguments unless the replacement ends with a question mark, while nginx return sends only what you write, so add is_args and args or use request_uri to keep them.

5. How many redirects can I put in .htaccess before it slows the site?

There is no fixed number, but every rule in .htaccess is read and tested on every request to that folder, including requests for images and scripts. A few dozen rules are rarely noticeable. Hundreds or thousands start to add measurable work. For long lists, move them into the main server configuration using RewriteMap on Apache or a map block on nginx, where the lookup is done once from a table instead of rule by rule.

6. How do I redirect a whole old domain to a new one on Apache or nginx?

Give the old domain its own virtual host or server block that does nothing but redirect. On Apache, a VirtualHost for the old names with Redirect 301 / https://www.example.co.uk/ sends every path to the same path on the new domain. On nginx, a server block with return 301 https://www.example.co.uk$request_uri does the same. Where old pages have new addresses, add those specific rules on the new site rather than chaining through the old one.

7. Why do I get a redirect loop after adding an HTTPS rule?

Usually because the server cannot see that the visitor is already on HTTPS. When a CDN, load balancer or proxy handles the secure connection and talks to your server over plain HTTP, a rule that checks whether HTTPS is off always fires, and the visitor is sent back to the same address. Check the forwarded protocol header from the proxy instead, or let the proxy handle the HTTPS redirect and remove the server rule.

Trace your server rules

The Redirect Checker follows each URL through its full chain, recording every status code, Location header and hop time, and flags long chains, loops and 302s that should be 301s.

Open Redirect Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Redirect Fixes  ·  Redirect Loops  ·  Migration Redirects  ·  Security Headers on Apache  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.