How to set Up Nginx 301 Redirects with an Nginx Map Example
Problem
- You migrated routes (e.g.,
/blog/*to/blogs/*) and renamed some slugs; you need fast 301s at the edge. - You want a scalable way to manage many old→new paths without app code changes.
- Errors like
nginx: [emerg] "map" directive is not allowed hereor “unexpected end of file, expecting”;or}are blocking you.
Solutions
- Define a
mapin the globalhttp {}block, not inside anyserver {}; use$uriso query strings don’t break matches.
# /etc/nginx/nginx.conf (inside http { ... }, outside any server { ... })
map_hash_bucket_size 256;
map $uri $new_uri {
default "";
include /etc/nginx/redirects.map;
}
- Create
/etc/nginx/redirects.mapwith oneold → new pathper line, and make sure to end each line with;.
# /etc/nginx/redirects.map
/resume /about;
/resume/ /about/;
/blogs/react-nextjs-comparison/ /blogs/next-js-vs-react-js/;
- Trigger the redirect early in the target
server {}block (place beforelocation /); preserve query strings.
# inside server { ... } for learnprogramming.us
if ($new_uri) {
return 301 https://$host$new_uri$is_args$args;
}
- Bulk path pattern (directory move) can use a simple regex
locationin the same server block.
# /blog/* → /blogs/*
location ~ ^/blog/(.*)$ {
return 301 /blogs/$1;
}
- Test the Nginx configuration (with
nginx -t), reload or restart Nginx, and finally use cURL to test if the 301 worked:
sudo nginx -t && sudo nginx -s reload
curl -I https://yourdomain.tld/resume
The final result from curl should look something like this with a 301 HTTP response:
HTTP/2 301
server: nginx/1.24.0
date: Tue, 26 Aug 2025 15:52:07 GMT
content-type: text/html
content-length: 169
location: https://yourdomain.tld/about
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
permissions-policy: geolocation=(), microphone=(), camera=()
strict-transport-security: max-age=31536000; includeSubDomains; preload
Nginx 301 Moved Permanently
- Use
return 301for permanent moves; clients and crawlers update links.
if ($new_uri) {
return 301 https://$host$new_uri$is_args$args;
}
Nginx Map Examples
- Exact match entries live in
redirects.map; add both with and without trailing slash if your site canonicalizes slashes.
/old-slug /new-slug;
/old-slug/ /new-slug/;
Nginx Map Directive: $new_uri Example
The $new_uri variable comes from the map directive. You set it up globally in the http {} block, then test it inside the right server {} block to trigger redirects. Without the if ($new_uri) test, the map never actually fires.
Step 1 — Define the map globally in http {}
http {
# Required for larger maps
map_hash_bucket_size 256;
# Define redirects
map $uri $new_uri {
default "";
include /etc/nginx/redirects.map;
}
# ... your server blocks here ...
}
Step 2 — Add the redirect check inside your server {}
This must be placed before any generic location / block, otherwise the catch-all proxy will swallow the request first.
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# Check the map; if there's a match, return a 301
if ($new_uri) {
return 301 https://$host$new_uri$is_args$args;
}
# Other location blocks here
location / {
proxy_pass http://127.0.0.1:9000;
}
}
Step 3 — Create /etc/nginx/redirects.map
# Old path New path
/resume /about;
/resume/ /about/;
/old-slug /new-slug;
/old-slug/ /new-slug/;
Why $new_uri is needed:
- The
mapsets up a lookup table from old paths to new ones. $new_uriis just a variable until you explicitly test it withif ($new_uri).- Placing it at the top of your
server {}ensures it triggers before requests fall through to your backend. - Adding
$is_args$argsensures query strings like?utm=fooare preserved in the redirect.
Things to Consider
mapmust be declared once athttpscope; it becomes available to all servers.- Prefer
$uriover$request_uriso?utm=...does not break lookups. - Put the
if ($new_uri)block before broadlocationmatches likelocation /. - Keep sitemap and internal links pointing only to new URLs to speed consolidation.
- Use
301(permanent) to pass link equity;302is temporary and not ideal here.
Nginx Map Regex Example
Sometimes you need pattern-based redirects instead of exact matches. The map directive in Nginx supports regular expressions when you prefix the key with ~.
Example: Redirect all .php pages to .html equivalents
http {
map $uri $new_uri {
default "";
# Regex match (must start with ~)
~^/old-section/(.*)\.php$ /new-section/$1.html;
}
}
server {
listen 443 ssl http2;
server_name example.com;
if ($new_uri) {
return 301 https://$host$new_uri$is_args$args;
}
location / {
proxy_pass http://127.0.0.1:9000;
}
}
Explanation:
~^/old-section/(.*)\.php$is a regex key.- The
(.*)captures whatever comes after/old-section/and before.php. $1reuses that capture in the replacement (/new-section/$1.html).- Any
/old-section/foo.phprequest will redirect permanently to/new-section/foo.html.
When to use Regex in a map:
- Redirecting file extensions (e.g.,
.php→.htmlor.asp→.aspx). - Moving whole path segments where only part of the slug changes.
- Bulk restructuring beyond one-to-one mapping.
NOTE: Regex maps are more CPU-intensive than simple lookups. Only use them when exact matches aren’t practical.
Gotchas
nginx: [emerg] "map" directive is not allowed heremeans yourmapis inside aserver {}; move it tohttp {}.unexpected end of file, expecting ";" or "}"usually means a missing;inredirects.map.- Trailing slash mismatches (
/resumevs/resume/) do not match unless you list both. - Using
$request_uricauses misses when query strings are present; use$uri. - Ordering matters; a catch-all
location /can swallow requests before the redirect fires. - Reload without testing can mask errors; always run
sudo nginx -tfirst. - Getting
200incurlbut404in browser may indicate the map didn’t match and the request fell through to the app.
Regex Backslash and Trailing Slash Targets
-
Redirect targets must match your site’s canonical slash style.
- If your canonical is
/about/, map old URLs to/about/. - If your canonical is
/about, map to/about.
- If your canonical is
-
Mixing them causes redirect chains (e.g.,
/resume/→/about→/about/). -
Worse, a missing slash can break subpaths entirely (e.g.,
/resume/redirecting to/aboutcould produce/aboutsomethinginstead of/about/something). -
Always test with
curl -Ito confirm the finalLocationmatches your site’s canonical URLs.
curl -I https://example.com/resume/
Expected output if canonical target is /about/:
HTTP/2 301
location: https://example.com/about/
Regex Map Example for Preserving Slashes
If you need to redirect an entire section while preserving any subpath after it, use a regex in your map file. This avoids broken paths when slashes are involved.
# /etc/nginx/redirects.map
# Regex match must start with ~
~^/resume/(.*)$ /about/$1;
Explanation:
-
~^/resume/(.*)$matches any URL starting with/resume/and captures the rest of the path into$1. -
/about/$1ensures the trailing content is preserved, so:/resume/→/about//resume/foo→/about/foo/resume/foo/bar→/about/foo/bar
-
This prevents the common mistake where
/resume/anythingwould otherwise collapse into/aboutanything.
Sources
- Nginx map module (official)
- Nginx variables index (official)
- Nginx
returndirective (official) - Nginx redirects — comprehensive tutorial
- StackOverflow: mass redirects from a list
- StackOverflow: Nginx Map Example
Further Investigation
- Explore
includeto load multiple files (e.g.,include /etc/nginx/redirects.d/*.map;) for large rule sets. - Review the
ssi(server side includes) module if you truly need HTML includes, not redirects. - Audit variables with the official “nginx variables list” to understand
$uri,$args,$is_args, and$request_uri. - Use a debug endpoint temporarily to print
$uriand$request_urifor tricky cases.
TL;DR
This section includes everything discussed above:
mapathttp {}scoperedirects.mapinclude$new_uricheck inside theserver {}- regex location for bulk redirects (
/blog/* → /blogs/*) - trailing slash considerations
- typical blog site basics (SSL, headers, proxy to app)
Use a map at http {} scope, a lookup file (/etc/nginx/redirects.map) for one-to-one redirects, and an early return 301 in the target server {}. Below is a complete production-ready nginx.conf for a typical blog site:
# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
# Logs
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Connection tuning
keepalive_timeout 65s;
# Redirect map (global, outside any server)
map_hash_bucket_size 256;
map $uri $new_uri {
default "";
include /etc/nginx/redirects.map;
}
# HTTP → HTTPS redirect
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
# Main HTTPS server
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Security headers
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
client_max_body_size 32m;
# Apply slug-based redirects from map
if ($new_uri) {
return 301 https://$host$new_uri$is_args$args;
}
# Bulk path migration (old Angular /blog/* → new Astro /blogs/*)
location ~ ^/blog/(.*)$ {
return 301 /blogs/$1;
}
# Pass everything else to app backend
location / {
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 30s;
proxy_send_timeout 30s;
}
# Example: convenience redirect for sitemap
location = /sitemap.xml {
return 301 /sitemap-index.xml;
}
}
}
/etc/nginx/redirects.map
# Old path New path
/resume /about;
/resume/ /about/;
/old-slug /new-slug;
/old-slug/ /new-slug/;
Notes
mapmust live inhttp {}, notserver {}.$uristrips query strings, so/resume?utm=foostill matches/resume.$is_args$argspreserves query strings when redirecting.- Always end map entries with
;. - For slugs with and without trailing slashes, add both.
- Place the
if ($new_uri)block before any catch-alllocation /.