NGINX / Server Setup

NGINX Rewrite Rules: The Complete Guide to URL Rewriting

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

URL rewriting is one of the most powerful features of NGINX, yet it remains one of the most misunderstood. If you have ever struggled with NGINX rewrite rules, copied configurations from Stack Overflow without understanding them, or wondered why your URL redirects are not working as expected, this guide is for you.

In this comprehensive guide, you will learn everything about NGINX URL rewriting: from basic redirects to complex regex patterns, from understanding the difference between return and rewrite to mastering capturing groups and rewrite flags. Every configuration in this article has been tested on Rocky Linux 9, AlmaLinux 10, and Rocky Linux 10 with NGINX versions ranging from 1.20 to 1.28.

What is NGINX URL Rewriting?

NGINX URL rewriting allows you to modify incoming request URLs before they are processed. This is essential for:

  • SEO migrations: Redirecting old URLs to new ones without losing search engine rankings
  • URL normalization: Enforcing consistent URL patterns (with or without www, trailing slashes)
  • Clean URLs: Converting query parameters to path-based URLs
  • Legacy URL support: Maintaining backwards compatibility when restructuring your site
  • Security: Blocking or redirecting malicious URL patterns

The NGINX rewrite module (ngx_http_rewrite_module) handles all URL manipulation. According to the official NGINX documentation, this module is compiled in by default and provides the rewrite, return, if, set, and break directives. Understanding how it works internally helps you write better, more efficient configurations.

The return Directive: Simple and Fast

For simple redirects and responses, the return directive is your best choice. It is faster than rewrite because it does not involve regex processing.

Basic Syntax

return code [text];
return code URL;
return URL;

HTTP Status Code Returns

The most common use case is returning HTTP status codes with redirects:

# 301 Permanent redirect
location = /old-page {
    return 301 /new-page;
}

# 302 Temporary redirect
location = /maintenance {
    return 302 /coming-soon;
}

# 410 Gone - for permanently removed content
location = /discontinued-product {
    return 410;
}

When you specify a URL starting with http://`,https://`, or $scheme, NGINX automatically treats it as a redirect:

# Redirect to external site
location = /partner {
    return 301 https://partner-site.com/landing;
}

# Redirect to same site with full URL
location = /secure {
    return 301 $scheme://$host/https-required;
}

Text Responses

You can also return plain text responses, useful for health checks or simple API endpoints:

# Health check endpoint
location = /health {
    return 200 "OK";
}

# Return JSON
location = /api/status {
    default_type application/json;
    return 200 '{"status": "healthy", "version": "1.0"}';
}

When to Use return vs rewrite

Use return when:

  • You need a simple redirect from one URL to another
  • The target URL is static or uses only NGINX variables
  • Performance is critical (no regex overhead)
  • You want to return a specific HTTP status code

Use rewrite when:

  • You need to match URL patterns with regular expressions
  • You need to capture parts of the URL for use in the replacement
  • You need to modify the URL internally without redirecting

The rewrite Directive: Pattern Matching Power

The rewrite directive uses PCRE regular expressions to match and transform URLs. It provides more flexibility than return but comes with additional complexity.

Basic Syntax

rewrite regex replacement [flag];
  • regex: A PCRE regular expression to match against the URI
  • replacement: The new URI, which can include captured groups
  • flag: Optional modifier that controls rewrite behavior

Simple Rewrite Examples

# Rewrite /articles/123 to /blog/post-123
rewrite ^/articles/([0-9]+)$ /blog/post-$1 permanent;

# Remove .html extension
rewrite ^/pages/(.+)[.]html$ /pages/$1 permanent;

# Convert query parameter to path
# /search?q=nginx becomes /search/nginx
if ($arg_q) {
    rewrite ^/search$ /search/$arg_q? permanent;
}

Understanding Regex in NGINX Rewrite Rules

NGINX uses PCRE (Perl Compatible Regular Expressions). If you are new to regular expressions in NGINX, you may also want to read our guide on NGINX location priority and regex matching which covers how location blocks interact with rewrite rules. Here are the most commonly used patterns:

Pattern Meaning Example
^ Start of string ^/api matches URLs starting with /api
$ End of string [.]html$ matches URLs ending in .html
. Any single character a.c matches abc, adc, a1c
* Zero or more of previous ab*c matches ac, abc, abbc
+ One or more of previous ab+c matches abc, abbc (not ac)
? Zero or one of previous colou?r matches color, colour
[abc] Character class [aeiou] matches any vowel
[0-9] Digit range [0-9]+ matches one or more digits
[a-z] Letter range [a-zA-Z]+ matches letters
(...) Capturing group ([0-9]+) captures digits as $1
[.] Literal dot [.]html matches .html literally

Important: In NGINX configurations, use [.] instead of \\. to match a literal dot. This avoids escaping issues and is more readable.

Capturing Groups: Extracting URL Parts

Capturing groups are one of the most powerful features of NGINX rewrite rules. They allow you to extract portions of the matched URL and use them in the replacement string.

Single Capturing Group

# Capture article ID
# /articles/12345 -> /blog?id=12345
rewrite ^/articles/([0-9]+)$ /blog?id=$1 last;

In this example:

  • ([0-9]+) captures one or more digits
  • $1 references the first captured group

Multiple Capturing Groups

# Capture category and product ID
# /shop/electronics/product/456 -> /store?cat=electronics&pid=456
rewrite ^/shop/([a-z]+)/product/([0-9]+)$ /store?cat=$1&pid=$2 last;

Captured groups are numbered left to right:

  • $1 = first capturing group (category)
  • $2 = second capturing group (product ID)
  • $0 = entire matched string

Date-Based URL Rewriting

A common use case is rewriting date-based blog URLs:

# /2024/01/my-article -> /blog?year=2024&month=01&slug=my-article
rewrite "^/([0-9]{4})/([0-9]{2})/([a-z0-9-]+)$" /blog?year=$1&month=$2&slug=$3 last;

Note the use of quotes around the regex when it contains curly braces {4}. This prevents NGINX from misinterpreting them as variable syntax.

Non-Capturing Groups

When you need grouping for alternation but do not need to capture, use (?:...):

# Match /api/v1 or /api/v2 without capturing the version
rewrite "^/api/(?:v1|v2)/(.+)$" /api/current/$1 last;

Rewrite Flags: Controlling Behavior

NGINX rewrite flags determine what happens after a rewrite rule matches. Understanding these flags is crucial for correct configuration.

The last Flag

The last flag stops processing the current set of rewrite directives and starts a new search for a matching location with the changed URI:

location /old {
    rewrite ^/old/(.*)$ /new/$1 last;
    # Code here will NOT execute after the rewrite
}

location /new {
    # Request will be processed here
    return 200 "New location";
}

Use last when: You want the rewritten URL to be matched against all location blocks again.

The break Flag

The break flag stops processing rewrite directives in the current context and continues processing the request within the current location:

location /download {
    rewrite ^/download/(.*)$ /files/$1 break;
    # The request continues in this location with the new URI
    root /var/www;
}

Use break when: You want to stay in the current location and serve the rewritten path directly.

The redirect Flag

The redirect flag returns a 302 (temporary) redirect to the client:

location /temp-move {
    rewrite ^/temp-move/(.*)$ /temporary/$1 redirect;
}

The browser receives a 302 response and makes a new request to the rewritten URL.

The permanent Flag

The permanent flag returns a 301 (permanent) redirect to the client:

location /old-section {
    rewrite ^/old-section/(.*)$ /new-section/$1 permanent;
}

Important for SEO: Use permanent (301) for URLs that have permanently moved. Search engines transfer link equity to the new URL. Use redirect (302) only for temporary moves.

Flag Comparison

Flag HTTP Response Browser Behavior Search Engine Treatment
last None (internal) Transparent N/A (internal)
break None (internal) Transparent N/A (internal)
redirect 302 New request Does not transfer SEO
permanent 301 New request, cached Transfers SEO value

Query String Handling

By default, NGINX appends the original query string to the rewritten URL. You can control this behavior:

Preserving Query Strings (Default)

# /old?foo=bar -> /new?foo=bar
rewrite ^/old$ /new last;

Dropping Query Strings

Add a ? at the end of the replacement to drop the original query string:

# /old?foo=bar -> /new (query string removed)
rewrite ^/old$ /new? last;

Replacing Query Strings

# /old?anything -> /new?replaced=yes
rewrite ^/old$ /new?replaced=yes? last;

The trailing ? removes the original query string, while your new parameters remain.

Preserving and Adding Parameters

# /old?existing=param -> /new?added=value&existing=param
rewrite ^/old$ /new?added=value last;

Real-World SEO Migration Scenarios

Let us look at practical NGINX rewrite configurations for common SEO scenarios.

Enforcing www or non-www

Choose one canonical version of your domain for SEO:

# Redirect non-www to www
server {
    listen 80;
    server_name example.com;
    return 301 $scheme://www.example.com$request_uri;
}

# Redirect www to non-www
server {
    listen 80;
    server_name www.example.com;
    return 301 $scheme://example.com$request_uri;
}

Enforcing HTTPS

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

Trailing Slash Normalization

# Add trailing slash (except for files)
location / {
    rewrite ^([^.]*[^/])$ $1/ permanent;
}

# Remove trailing slash
location / {
    rewrite ^/(.*)/$ /$1 permanent;
}

Blog Platform Migration

When migrating from WordPress to a static site or different CMS:

# WordPress to clean URLs
# /index.php?p=123 -> /post/123
if ($arg_p) {
    return 301 /post/$arg_p;
}

# Category permalinks
# /?cat=5 -> /category/5
if ($arg_cat) {
    return 301 /category/$arg_cat;
}

# Old date-based permalinks
# /2023/01/15/post-title -> /blog/post-title
rewrite "^/[0-9]{4}/[0-9]{2}/[0-9]{2}/(.+)$" /blog/$1 permanent;

E-commerce URL Restructuring

# Old product URLs to new structure
# /products.php?id=123 -> /shop/product/123
if ($arg_id) {
    rewrite ^/products[.]php$ /shop/product/$arg_id? permanent;
}

# Category restructure
# /catalog/category-name/product-name -> /shop/category-name/product-name
rewrite ^/catalog/(.+)$ /shop/$1 permanent;

Handling Removed Content

# Return 410 Gone for permanently removed content
location ~ ^/discontinued/ {
    return 410;
}

# Redirect removed pages to relevant alternatives
location = /old-service {
    return 301 /new-service;
}

Advanced Rewrite Techniques

Case-Insensitive Matching

Use the (?i) modifier for case-insensitive matching:

# Match /About, /about, /ABOUT, etc.
rewrite "(?i)^/about$" /about-us permanent;

Conditional Rewrites with if

While if in NGINX has caveats, it is sometimes necessary:

# Redirect mobile users
if ($http_user_agent ~* "mobile|android|iphone") {
    rewrite ^/$ /mobile/ redirect;
}

# Redirect based on query parameter
if ($arg_legacy = "1") {
    return 301 /modern$request_uri;
}

Warning: The if directive in NGINX is famously problematic. As the NGINX documentation states: “if is evil”. Use it only when absolutely necessary and test thoroughly.

Using Maps for Complex Rewrites

For many redirects, use a map block instead of multiple rewrite rules:

map $uri $new_uri {
    /old-page-1    /new-page-1;
    /old-page-2    /new-page-2;
    /old-page-3    /new-page-3;
    ~^/blog/(.*)   /articles/$1;
    default        "";
}

server {
    if ($new_uri) {
        return 301 $new_uri;
    }
}

Maps are more efficient than multiple rewrite rules because they are evaluated once at configuration load time. For another example of using maps effectively, see our article on tuning proxy_buffer_size in NGINX which demonstrates how to calculate values dynamically.

Rewrite Logging for Debugging

Enable rewrite logging to troubleshoot complex rules:

error_log /var/log/nginx/error.log notice;
rewrite_log on;

This logs all rewrite operations to the error log at the notice level. Check the log to see exactly how your rules are being processed.

Common NGINX Rewrite Mistakes

Mistake 1: Infinite Rewrite Loops

# BAD: Creates infinite loop
rewrite ^/(.*)$ /prefix/$1 last;

This matches /prefix/something and rewrites to /prefix/prefix/something, creating an infinite loop. NGINX will stop after 10 iterations and return a 500 error.

Fix: Add a condition to prevent matching already-rewritten URLs:

# GOOD: Check before rewriting
location / {
    if ($uri !~ ^/prefix/) {
        rewrite ^/(.*)$ /prefix/$1 last;
    }
}

Mistake 2: Using rewrite When return Would Suffice

# Inefficient
rewrite ^/about$ /about-us permanent;

# Better
location = /about {
    return 301 /about-us;
}

The return directive is faster and more readable for simple redirects.

Mistake 3: Forgetting the Anchor

# Matches /about and /about-us and /about-contact
rewrite /about /about-us permanent;

# Only matches exactly /about
rewrite ^/about$ /about-us permanent;

Always use ^ and $ anchors to match exact patterns.

Mistake 4: Wrong Escaping

# BAD: Backslash escaping issues
rewrite ^/file\.html$ /file permanent;

# GOOD: Use character class for literal dot
rewrite ^/file[.]html$ /file permanent;

Mistake 5: Not Testing Query String Behavior

# Forgets that query strings are preserved
rewrite ^/old$ /new permanent;
# /old?important=data -> /new?important=data (might be unexpected)

# Explicit about query string handling
rewrite ^/old$ /new? permanent;  # Drops query string

Performance Considerations

NGINX compiles rewrite rules into bytecode at configuration load time, making them very efficient. However, some practices can impact performance:

  1. Use return over rewrite when possible: No regex processing required
  2. Use exact matches: location = /path is faster than regex locations
  3. Use maps for many redirects: More efficient than multiple rewrite rules
  4. Order rules by frequency: Put most common rules first
  5. Avoid nested if conditions: Each condition adds processing overhead

For additional performance tuning tips, see our guide on NGINX rate limiting which covers how to protect your server while maintaining performance.

Installing NGINX and Enabling the Rewrite Module

On RHEL-based systems (Rocky Linux, AlmaLinux, CentOS), the rewrite module is included by default:

dnf install nginx
systemctl enable --now nginx

Verify the rewrite module is included:

nginx -V 2>&1 | grep -o with-http_rewrite_module

The rewrite module requires PCRE. On most systems, this is installed automatically as a dependency.

Testing Your Rewrite Rules

Always test rewrite rules before deploying to production:

# Test configuration syntax
nginx -t

# Test a specific URL with curl
curl -I http://localhost/old-url

# Follow redirects to see the chain
curl -ILs http://localhost/old-url | grep -E "^HTTP|^Location"

# Check the error log for rewrite debugging
tail -f /var/log/nginx/error.log

Conclusion

NGINX rewrite rules are a powerful tool for URL manipulation, SEO migrations, and maintaining clean URL structures. The key points to remember:

  • Use return for simple redirects without pattern matching
  • Use rewrite when you need regex matching and capturing groups
  • Choose the right flag: last for re-evaluation, break to stay in location, permanent for SEO
  • Always test your rules with nginx -t and curl before deployment
  • Enable rewrite_log when debugging complex configurations
  • Use maps for many redirects instead of multiple rewrite rules

With proper understanding of NGINX URL rewriting, you can handle any URL transformation scenario while maintaining excellent SEO and performance.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.