How to Host Static HTML Alongside WordPress with Clean URLs on Nginx

Learn how to configure Nginx to serve standalone HTML files alongside a WordPress installation while removing .html extensions for a professional, clean URL structure.

WordPress is an incredibly powerful CMS, but sometimes you need something lighter. Perhaps you have a custom-built tool, a legacy landing page, or a high-performance utility script—like an online image watermarker or resizer—that exists simply as a static .html file.

The challenge? You want these files to live in the root directory alongside WordPress, but you don’t want your URLs to look like example.com/tool.html. You want them to look clean and professional, like example.com/tool, matching your WordPress permalink structure.

In this guide, I’ll show you how to configure Nginx to serve static HTML files without the extension, while seamlessly falling back to WordPress for everything else.

The Goal

We want to achieve three things:

  1. Serve Static Files: Allow files like watermark-image.html to be accessed.
  2. Clean URLs: Users should visit /watermark-image, not /watermark-image.html.
  3. WordPress Compatibility: If a static file doesn’t exist, the server should load WordPress as usual.

The Problem with Standard Configurations

Standard Nginx configurations for WordPress usually send every request that isn’t a physical file directly to index.php. If you simply upload tool.html and try to visit /tool, Nginx won’t find a file named “tool,” so it sends the request to WordPress, which returns a 404 error.

The Nginx Solution

To fix this, we need to use the try_files directive and some strategic redirects.

1. Redirecting .html Extensions

First, if someone (or a search engine) visits the old version of the URL with the extension (e.g., /tool.html), we want to 301 Redirect them to the clean version (/tool).

We use $request_uri to check the actual request from the browser. This is crucial to prevent “redirect loops.”

Nginx

if ($request_uri ~ ^/(.*)\.html$) {
    return 301 /$1;
}

2. The try_files Magic

Next, we tell Nginx how to look for files. We modify the location / block.

Nginx

location / {
    try_files $uri $uri.html $uri/ /index.php?$args;
}

Here is exactly what happens when a user visits /watermark-image:

  1. $uri: Nginx checks if a file named watermark-image exists. (It doesn’t).
  2. $uri.html: Nginx checks if watermark-image.html exists. Success! It serves this file, but keeps the URL in the browser as /watermark-image.
  3. $uri/: If neither of the above works, it checks if it’s a directory.
  4. index.php: If all else fails, it assumes it’s a WordPress page and hands it off to PHP.

Dealing with the Homepage (/index.html)

A common side effect of this setup is that your homepage might be accessible via /index or /index.html, which is bad for SEO (duplicate content). We add a rule to strip this away:

Nginx

if ($request_uri ~* "^/index(\.html)?$") {
    return 301 /;
}

Summary

By combining these rules, you get the best of both worlds: the flexibility of raw HTML/JS tools and the power of the WordPress CMS, all unified under a clean, professional URL structure.

This setup is particularly useful for sites hosting online utilities, calculators, or single-page applications (SPAs) within a WordPress ecosystem.

server {
    listen 80;
    server_name www.yourdomain.com yourdomain.com;
    # Redirect all HTTP traffic to HTTPS
    return 301 https://$server_name$request_uri;
}

# HTTPS Main Configuration
server {
    listen 443 ssl http2;
    server_name www.yourdomain.com .yourdomain.com.com;

    root /home/wwwroot/www.yourdomain.com;
    index index.html index.htm index.php;

    # SSL Configuration
    ssl_certificate /usr/local/nginx/conf/ssl/www.yourdomain.com/fullchain.cer;
    ssl_certificate_key /usr/local/nginx/conf/ssl/www.yourdomain.com/www.yourdomain.com.key;
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers "TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5";
    ssl_session_cache builtin:1000 shared:SSL:10m;
    ssl_dhparam /usr/local/nginx/conf/ssl/dhparam.pem;

    # --- Core Logic: Clean URLs for Static Files & WordPress ---

    # 1. Clean up Homepage URL
    # If the request is specifically /index.html or /index, 301 redirect to root /
    if ($request_uri ~* "^/index(\.html)?$") {
        return 301 /;
    }

    # 2. Enforce Extension Removal (Redirect .html to extensionless)
    # Checks the raw request ($request_uri) to avoid infinite loops with try_files.
    # If a user visits /page.html, they are redirected to /page
    if ($request_uri ~ ^/(.*)\.html$) {
        return 301 /$1;
    }

    # 3. Main Routing Logic
    location / {
        # Sequence is critical here:
        # 1. $uri: Check if the file exists exactly as requested.
        # 2. $uri.html: Check if the file exists with .html appended (enables accessing /file as /file.html).
        # 3. $uri/: Check if it is a directory.
        # 4. /index.php?$args: Fallback to WordPress for everything else.
        try_files $uri $uri.html $uri/ /index.php?$args;

Test it:

nginx -t

Reload nginx

nginx -s reload 

or

service nginx reload