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
returnsends a response immediately. Use it for any redirect where you know the target. It is fast and easy to read.rewritematches the URI against a regular expression and changes it. With thepermanentflag 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 matchinglocationwith the changed URI.break: stop rewrites and serve from the currentlocation.
Redirect a single page
Use an exact-match location. The = means only that exact path matches:
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:
map $uri $redirect_target {
default "";
/old-page.html /new-page/;
/about-us.php /about/;
/contact.htm /contact/;
}Then, inside the site's server block:
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:
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:
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:
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:
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:
Redirect 301 /old.html /new.htmlNginx:
location = /old.html { return 301 /new.html; }Apache front controller (WordPress and most PHP apps):
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [L]Nginx:
location / {
try_files $uri $uri/ /index.php?$args;
}Apache folder move with a capture:
RewriteRule ^shop/(.*)$ /store/$1 [R=301,L]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:
sudo nginx -t && sudo systemctl reload nginxCheck each redirect with curl -I:
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=mailTo test a long list, put the old paths in old.txt, one per line, and loop over it:
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.txtTroubleshooting
- Redirect is ignored: another
locationmatched 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. Withrewrite, 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
/blogto/blog/. Use an exact matchlocation = /blog. - Old 301 still applies after a change: browsers cache 301s. Test with
curlor a private window, and use 302 while experimenting. nginx -treports "unknown directive": a rule was pasted from Apache. Convert it as shown above.
Next steps
- Pick a single hostname: Redirect www to non-www in Nginx.
- Coming from Apache? Switch from Apache to Nginx.
- Moving the site to new hosting? A Linux VPS gives you full control over redirects.
