My basic goal is to serve the following clean URLs with nginx:

  • / serves /index.html
  • /abc/ serves /abc/index.html
  • /abc/def serves /abc/def.html
  • /abc redirects to /abc/

In order to have canonical names for each resource, I also want to normalize any URL with superfluous file names or extensions:

  • /index.html redirects to /
  • /abc/index.html redirects to /abc/
  • /abc/def.html redirects to /abc/def

The directives I thought would accomplish this:

index index.html;
try_files $uri.html $uri $uri/ =404;

# Redirect */index and */index.html to *.
rewrite ^(.*)/index(\.html)?$ $1 permanent;
# Redirect *.html to *.
rewrite ^(.+)\.html$          $1 permanent;

However, the result of this is different than I expected:

  • /, /index and /index.html all redirect to / (loop).
  • /abc and /abc/ both redirect to /abc/ (loop).

(It works as designed for /abc/def.html and /abc/def; only the directory URLs don't work.)

I'm not sure what is happening here; maybe I'm misunderstanding how the rewrite works?

(I already tried using location blocks instead, but this also results in loops as try_files performs an internal redirect to the location block that sends the HTTP 301.)

Edit: Fundamentally, I need something like a location block that only matches the original request URI, but is ignored for the purpose of internal redirects, so it doesn't create a loop in combination with the try_files directive.


You might be looking for a solution like the one explained here:

server {
    listen       80;
    server_name  mysite.com;

    index index.html;
    root /var/www/mysite/public;

    location / { 
        try_files $uri $uri/ @htmlext;

    location ~ \.html$ {
        try_files $uri =404;

    location @htmlext {
        rewrite ^(.*)$ $1.html last;
| improve this answer | |
  • Hm, that doesn't look quite correct - although the clean URLs are now served, the non-clean ones are also served rather than redirected. That behavior could have also been accomplished with only the try_files $uri $uri.html $uri/ =404; index index.html; directives. Did I miss something? – Christoph Burschka Feb 13 '18 at 17:04

I believe I have found a solution, though I'm not experienced enough to tell if this could break in special cases or could be solved more easily in another way.

Basically, the problem is that a location ~ ... {} block does not only get matched on the original request URI, but also on the result of try_files and other rewrites. So if I have a location block to strip off the index.html or .html with a redirect, then it will not only run when a client requests index.html or abc.html directly, but also if the client requests / or abc and the server internally redirects these to /index.html and abc.html respectively, causing a redirect loop.

However, the redirect module provides an if directive which can check the $request_uri variable - this remains unchanged by internal redirects:

index index.html;
try_files $uri $uri.html $uri/ =404;

# like "location ~", but only for matching the original request. 
if ($request_uri ~ /index(.html)?$) {
  rewrite ^(.*/)index(.html)?$ $1 permanent;
if ($request_uri ~ \.html$) {
  rewrite ^(.*)\.html$ $1 permanent;

(Note that all of these directives now exist in the server context, without any location blocks.)

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.