Fork me on GitHub

Note! In most cases redirects are better done in WordPress/PHP e.g. the Redirection plugin, using the https://codex.wordpress.org/Rewrite_API or with custom code in e.g. theme functions.php. Use Nginx redirects only if there is no WordPress way to do it, for example when diverting traffic from the index.php to another custom PHP script on the same site.

Basic redirects in WordPress/PHP

In PHP code you can express whatever redirection logic you want without being confined to the limited features on Nginx. For example there could be a file mu-plugins/redirects.php that would include:

<?php
// Redirect any requests for www.esimerkki.fi or esimerkki.fi to example.com/fi/
if ( isset($_SERVER['HTTP_HOST']) && strpos($_SERVER['HTTP_HOST'], 'www.esimerkki.fi') !== false ) {
  header("Location: https://example.com/fi/", true, 301);
  die();
}

Another more elaborate one example would be:

switch ($_SERVER['HTTP_HOST']) {

  # Enforce no www
  # Use 301 to make redirect permanent and cached
  # Use 302 for temporary (non-cached) redirects
  case "www.example.com":
    header("Location: https://example.com/", true, 301);
    break;

  # Multiple extra domains to same canonical domain
  # Note! Many plugins already do this automatically, e.g. Seravo Plugin or Polylang
  case "example.org":
  case "exmple.net":
  case "example.info":
    header("Location: https://example.com/", true, 301);
    break;

  # Localized domain to subfolder
  case "example.fi":
    header("Location: https://example.com/fi/", true, 301);
    break;

  # Localized domain to subfolder
  case "example.de":
    header("Location: https://example.com/de/", true, 301);
    break;

  default:
    header("Location: https://www.happy-or-not.com/en/", true, 301);

}

Force canonical domain

Google and other search engines don’t like if the exact same content is served on multiple websites. If a website, say example.com, also has the domain example.net if should not serve the same content on both domains but instead choose which domain is the canonical domain and then redirect all alternative domains to it. The same applies for subdomains. Websites should choose if they are available under www.example.com or example.com and then redirect the other to the one.

Normally website developers don’t need to bother with this, since WordPress core automatically redirects visitors to the canonical domain based on the siteurl setting. Also any site with the Seravo Plugin will automatically enforce both the canonical domain and the use of HTTPS to ensure all visits are protected.

However, certain plugins (e.g. WPML) that mess up the WordPress Rewrite API settings might break this, and in those rare cases a developer might need to create a custom /data/wordpress/htdocs/wp-content/mu-plugins/redirect.php file with the following contents:

<?php
// Redirect any requests for www.example.com to example.com (non-www)
if ( isset($_SERVER['HTTP_HOST']) && isset($_SERVER['REQUEST_URI']) && $_SERVER['HTTP_HOST'] == 'www.example.com' ) {
  header("Location: https://example.com/" . $_SERVER['REQUEST_URI'], true, 301);
  die();
}

Remember to test the redirect with curl to ensure it works!

Basic redirects in Nginx

Redirects from http://your-site.com/original -> https://example.com/new/url/

rewrite ^/original(.*)$ https://example.com/new/url$1 permanent;

Rewrite all old *.html files to WordPress pages with pretty URLs:

rewrite ^/([0-9a-z-]+)\.html /$1/ permanent;

Serve country pages (example.es, example.de etc) from custom PHP file

if ($host ~ "example.es|example.de") {
  rewrite ^(/)?$ /country-pages/index.php last;
}

Please use something like Rexexpal to test your regular expressions and be sure to use curl -IL to test your redirects without having to hassle with the cache of a regular browser.

Redirecting domains in Nginx

If you have multiple domains for your site and want to only use one of them:

if ($host ~ "old-subdomain.your-old-site.com") {
  return 301 https://your-site.com$request_uri;
}

Testing redirects

Please use curl to test redirects. Using a browser for testing will not work as the browser in most cases will cache the first redirect and after that no changes will be visible when testing with a broser. Using curl with header Pragma:no-cache ensures there is no caching at all and it will print location: headers that show clearly what the redirect is.

Example:

$ curl -IL -H Pragma:no-cache www.example.org
HTTP/1.1 200 OK
X-Cache: BYPASS
Location: https://example.com/
Content-Length: 100

HTTP/1.1 200 OK
X-Cache: BYPASS
Content-Length: 1270

Reviewing WordPress Rewrite API contents

If a site has many redirect plugins, language plugins and maybe even custom WP Rewrite API rules registered in the theme the overall situation can become complex. Easiest way to review all current rewrites is to use wp-cli:

$ wp rewrite
usage: wp rewrite flush [--hard]
   or: wp rewrite list [--match=<url>] [--source=<source>] [--fields=<fields>] [--format=<format>]
   or: wp rewrite structure <permastruct> [--category-base=<base>] [--tag-base=<base>] [--hard]


$ wp rewrite flush
Success: Rewrite rules flushed.

$ wp rewrite list
+---------------------------------------+------------------------------------------+--------------------+
| match                                 | query                                    | source             |
+---------------------------------------+------------------------------------------+--------------------+
| sitemap\.xml$                         | index.php?the_seo_framework_sitemap=xml  | other              |
| sitemap\.xsl$                         | index.php?the_seo_framework_sitemap=xsl  | other              |
| ^wp-json/?$                           | index.php?rest_route=/                   | other              |
| ^wp-json/(.*)?                        | index.php?rest_route=/$matches[1]        | other              |
| ^index.php/wp-json/?$                 | index.php?rest_route=/                   | other              |
| ^index.php/wp-json/(.*)?              | index.php?rest_route=/$matches[1]        | other              |
| case/?$                               | index.php?post_type=case                 | other              |
| case/feed/(feed|rdf|rss|rss2|atom)/?$ | index.php?post_type=case&feed=$matches[1 | other              |
...