Websites & apps

Nginx 301 redirects and rewrite rules explained

Learn when to use return and when to use rewrite in Nginx, then redirect single pages, whole folders and old domains, and convert common.htaccess rules.

IntermediateLinux server5 min read

When you move pages, rename a folder or change domains, a 301 redirect sends visitors and search engines to the new address and carries most of the old page's ranking with it. Nginx has two tools for this: return and rewrite. This guide explains the difference, gives tested patterns for the common cases, and shows how to translate Apache .htaccess rules, which Nginx does not read.

Before you start

  • A Linux VPS with Nginx serving your site.
  • The site's server block file, for example /etc/nginx/sites-available/example.com (Ubuntu/Debian) or /etc/nginx/conf.d/example.com.conf (AlmaLinux/Rocky).
  • A list of old and new URLs. A spreadsheet export is fine.
  • A backup copy of the config before you start: sudo cp /etc/nginx/sites-available/example.com ~/example.com.bak.

return vs rewrite

  • return sends a response immediately. Use it for any redirect where you know the target. It is fast and easy to read.
  • rewrite matches the URI against a regular expression and changes it. With the permanent flag it sends a 301; without a flag it changes the URI internally and Nginx keeps processing, which the visitor never sees.

Rule of thumb: use return 301 inside a location for redirects, and keep rewrite for cases where you need regex captures and there is no clean location alternative.

The rewrite flags you will meet:

  • permanent: send a 301.
  • redirect: send a 302 (temporary).
  • last: stop rewrites and search for a new matching location with the changed URI.
  • break: stop rewrites and serve from the current location.

Redirect a single page

Use an exact-match location. The = means only that exact path matches:

Nginx
location = /old-page.html {
    return 301 /new-page/;
}

A relative target keeps the same scheme and host. To send it somewhere else, use a full URL: return 301 https://example.com/new-page/;.

For many single-page redirects, a map keeps them in one tidy list. Put this in the http context, for example in a new file /etc/nginx/conf.d/redirects-map.conf:

Nginx
map $uri $redirect_target {
    default         "";
    /old-page.html  /new-page/;
    /about-us.php   /about/;
    /contact.htm    /contact/;
}

Then, inside the site's server block:

Nginx
if ($redirect_target) {
    return 301 $redirect_target;
}

Using if with only a return inside is safe. A map lookup is fast even with thousands of entries.

Redirect a whole folder

To move everything under /blog/ to /news/ and keep the rest of the path:

Nginx
location ^~ /blog/ {
    rewrite ^/blog/(.*)$ /news/$1 permanent;
}

(.*) captures everything after /blog/ and $1 puts it back. The query string is added automatically. ^~ tells Nginx to use this prefix match and skip the regex locations, so a PHP rule elsewhere cannot catch these requests first.

The same with return and a regex location:

Nginx
location ~ ^/blog/(.*)$ {
    return 301 /news/$1$is_args$args;
}

With return you must add the query string yourself, which is what $is_args$args does.

Redirect an old domain

Give the old domain its own server block that sends every path to the new one:

Nginx
server {
    listen 80;
    listen [::]:80;
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name old-example.com www.old-example.com;

    ssl_certificate     /etc/letsencrypt/live/old-example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/old-example.com/privkey.pem;

    return 301 https://example.com$request_uri;
}

Keep the old domain registered, pointed at the server and covered by a valid certificate for as long as you want the redirect to work. Search engines need months to move everything over.

Rewrite without redirecting

Sometimes you want a clean URL that maps to a script without the visitor seeing a redirect. For example, /product/42 served by /product.php?id=42:

Nginx
location ~ ^/product/(\d+)$ {
    rewrite ^/product/(\d+)$ /product.php?id=$1 last;
}

last makes Nginx look for a new location for /product.php, so your PHP block handles it. Most modern apps (WordPress, Laravel) do not need this; they use try_files instead. See Fix Nginx 404 errors with try_files.

Convert common.htaccess rules

Nginx ignores .htaccess files. Here are the most common Apache rules and their Nginx versions.

Apache single redirect:

Apache
Redirect 301 /old.html /new.html

Nginx:

Nginx
location = /old.html { return 301 /new.html; }

Apache front controller (WordPress and most PHP apps):

Apache
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [L]

Nginx:

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

Apache folder move with a capture:

Apache
RewriteRule ^shop/(.*)$ /store/$1 [R=301,L]

Nginx:

Nginx
rewrite ^/shop/(.*)$ /store/$1 permanent;

Note that Apache rules in .htaccess match without the leading slash, while Nginx always includes it.

Apply and check

Test and reload:

Bash
sudo nginx -t && sudo systemctl reload nginx

Check each redirect with curl -I:

Bash
curl -I https://example.com/old-page.html
curl -I "https://example.com/blog/2024/hello?ref=mail"

Expected:

HTTP/2 301
location: https://example.com/news/2024/hello?ref=mail

To test a long list, put the old paths in old.txt, one per line, and loop over it:

Bash
while read -r p; do
  printf '%s -> ' "$p"
  curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' "https://example.com$p"
done < old.txt

Troubleshooting

  • Redirect is ignored: another location matched first. Exact (=) and ^~ prefix matches win over regex locations; regex locations are checked in file order. Move or tighten the rule.
  • Query string is lost or doubled: with return, add $is_args$args. With rewrite, end the target with ? to drop the original query string, or leave it to keep it.
  • Redirect loops: the target also matches the rule, for example redirecting /blog to /blog/. Use an exact match location = /blog.
  • Old 301 still applies after a change: browsers cache 301s. Test with curl or a private window, and use 302 while experimenting.
  • nginx -t reports "unknown directive": a rule was pasted from Apache. Convert it as shown above.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.