Back to Learn

Remove .php / .html Extension from URLs | NOC.org

Why Remove File Extensions from URLs?

URLs like example.com/about.php or example.com/services.html expose your server's technology stack and create rigid URLs tied to specific file types. If you later migrate from PHP to a different language, all your URLs break. Clean URLs like example.com/about are technology-agnostic, shorter, easier to remember, and generally preferred by search engines. While Google has stated that file extensions do not directly affect rankings, clean URLs improve click-through rates in search results and make your site look more professional.

Removing file extensions also provides a layer of security through obscurity. When users and bots cannot tell which server-side technology you are running, it makes targeted attacks slightly harder. More importantly, it gives you the flexibility to change your backend technology without breaking existing links and bookmarks.

Apache: Removing Extensions with mod_rewrite

Apache's mod_rewrite module is the standard way to create clean URLs. The rules go in your .htaccess file (or in your VirtualHost configuration for better performance).

Removing .php Extension

RewriteEngine On

# Redirect requests with .php extension to clean URL (301 permanent)
RewriteCond %{THE_REQUEST} \s/(.+?)\.php[\s?] [NC]
RewriteRule ^ /%1 [R=301,L]

# Internally rewrite clean URL to the actual .php file
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.php -f
RewriteRule ^(.+?)/?$ $1.php [L]

How these rules work:

  1. The first block catches any incoming request that contains .php in the URL and issues a 301 redirect to the same URL without the extension. The %{THE_REQUEST} variable is used instead of %{REQUEST_URI} to prevent redirect loops, because THE_REQUEST contains the original request string before any rewriting.
  2. The second block takes the clean URL (without extension) and internally maps it to the actual .php file on disk. The RewriteCond checks ensure the request is not a directory and that a matching .php file exists.

Removing .html Extension

RewriteEngine On

# Redirect .html requests to clean URL
RewriteCond %{THE_REQUEST} \s/(.+?)\.html[\s?] [NC]
RewriteRule ^ /%1 [R=301,L]

# Internally rewrite clean URL to .html file
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.html -f
RewriteRule ^(.+?)/?$ $1.html [L]

Removing Both .php and .html Extensions

RewriteEngine On

# Redirect .php requests to clean URLs
RewriteCond %{THE_REQUEST} \s/(.+?)\.php[\s?] [NC]
RewriteRule ^ /%1 [R=301,L]

# Redirect .html requests to clean URLs
RewriteCond %{THE_REQUEST} \s/(.+?)\.html[\s?] [NC]
RewriteRule ^ /%1 [R=301,L]

# Internally rewrite to .php if the file exists
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.php -f
RewriteRule ^(.+?)/?$ $1.php [L]

# Internally rewrite to .html if the file exists
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.html -f
RewriteRule ^(.+?)/?$ $1.html [L]

The order matters here. PHP files are checked first, so if you have both about.php and about.html, the PHP file takes priority. Adjust the order based on your site's primary technology.

Handling Trailing Slashes

Trailing slashes are a common source of duplicate content and redirect loops. You should choose a consistent policy: either always include a trailing slash or never include one. Most sites that remove file extensions opt for no trailing slash:

Remove Trailing Slashes (Recommended)

# Remove trailing slash from non-directory URLs
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [R=301,L]

Force Trailing Slashes

# Add trailing slash to non-file URLs
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !(.+)/$
RewriteRule ^(.+)$ $1/ [R=301,L]

Place your trailing slash rules before the extension removal rules to ensure correct processing order. Be careful with the directory check (!-d) to avoid redirecting actual directory URLs that legitimately need trailing slashes.

Nginx: Removing Extensions with try_files

Nginx does not use .htaccess files. All configuration goes in the server block, typically in /etc/nginx/sites-available/your-site. Nginx's try_files directive is simpler and more performant than Apache's mod_rewrite for this purpose.

Removing .php Extension in Nginx

server {
    listen 443 ssl http2;
    server_name example.com;
    root /var/www/example.com;
    index index.php index.html;

    # Redirect .php URLs to clean versions
    location ~ \.php$ {
        # Only redirect if the URI was explicitly requested with .php
        if ($request_uri ~ ^(.+)\.php$) {
            return 301 $1;
        }
        # Handle PHP processing for internal rewrites
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    # Try clean URL, then with .php extension, then 404
    location / {
        try_files $uri $uri/ $uri.php $uri/index.php =404;
    }
}

For more on setting up Nginx with PHP, see our guide on configuring Nginx with PHP and SSL on Ubuntu.

Removing .html Extension in Nginx

server {
    listen 443 ssl http2;
    server_name example.com;
    root /var/www/example.com;
    index index.html;

    # Redirect .html URLs to clean versions
    if ($request_uri ~ ^(.+)\.html$) {
        return 301 $1;
    }

    location / {
        try_files $uri $uri/ $uri.html $uri/index.html =404;
    }
}

Handling Both Extensions in Nginx

server {
    listen 443 ssl http2;
    server_name example.com;
    root /var/www/example.com;
    index index.php index.html;

    # Redirect any URL ending in .php or .html to the clean version
    if ($request_uri ~ ^(.+)\.(php|html)$) {
        return 301 $1;
    }

    # Try the URI as-is, then as a directory, then with .php, then with .html
    location / {
        try_files $uri $uri/ $uri.php $uri.html =404;
    }

    # Process PHP files (reached via internal rewrite from try_files)
    location ~ \.php$ {
        internal;  # Only allow internal rewrites, not direct access
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

The internal directive on the PHP location block ensures that direct requests to .php files cannot bypass the redirect. Only internal rewrites from try_files can reach the PHP handler.

Avoiding Redirect Loops

Redirect loops are the most common problem when implementing clean URLs. They occur when the redirect rule and the rewrite rule conflict with each other. Here are the key principles to avoid them:

Apache: Use %{THE_REQUEST}

Always use %{THE_REQUEST} in your redirect conditions, not %{REQUEST_URI}:

# CORRECT: Uses THE_REQUEST (original request, not rewritten)
RewriteCond %{THE_REQUEST} \s/(.+?)\.php[\s?] [NC]
RewriteRule ^ /%1 [R=301,L]

# WRONG: Uses REQUEST_URI (changes after internal rewrite, causing loop)
RewriteCond %{REQUEST_URI} \.php$
RewriteRule ^(.+)\.php$ /$1 [R=301,L]

The %{THE_REQUEST} variable contains the original HTTP request line (e.g., GET /about.php HTTP/1.1) and is not modified by internal rewrites. The %{REQUEST_URI} variable changes after each rewrite, so using it in a redirect condition creates infinite loops.

Nginx: Use $request_uri

In Nginx, use $request_uri for the redirect check because it contains the original unmodified URI:

# CORRECT: Checks original request URI
if ($request_uri ~ ^(.+)\.php$) {
    return 301 $1;
}

# The try_files rewrite does not affect $request_uri, so no loop occurs

301 Redirects from Old URLs

When you remove file extensions, existing links, bookmarks, and search engine indexes still point to the old URLs with extensions. The 301 redirect rules above handle this automatically, but you should verify they work correctly:

# Test that old URLs redirect properly
curl -I https://example.com/about.php
# Expected: HTTP/1.1 301 Moved Permanently
# Location: https://example.com/about

curl -I https://example.com/services.html
# Expected: HTTP/1.1 301 Moved Permanently
# Location: https://example.com/services

# Test that clean URLs return 200
curl -I https://example.com/about
# Expected: HTTP/1.1 200 OK

# Test for redirect loops (should stop after one redirect)
curl -I -L --max-redirs 5 https://example.com/about.php
# Expected: 301 -> 200 (two responses, no loop)

The 301 (permanent) status code tells search engines to update their index to the new clean URL and transfer link equity. This is crucial for maintaining your SEO rankings during the transition.

Preserving Query Strings

Make sure your redirect rules preserve query string parameters. In Apache, the [QSA] flag (Query String Append) handles this, but with the %{THE_REQUEST} approach, query strings are typically preserved automatically:

# Test with query strings
curl -I "https://example.com/search.php?q=test&page=2"
# Expected: 301 redirect to /search?q=test&page=2

# If query strings are being dropped, add QSA flag:
RewriteRule ^ /%1 [R=301,L,QSA]

SEO Implications

Removing file extensions has several SEO benefits:

  • Cleaner appearance in SERPs — URLs without extensions look more professional and are more likely to be clicked
  • Technology independence — You can change your backend without losing link equity
  • Consistent URL structure — Easier to implement a coherent URL hierarchy
  • Reduced duplicate content — With proper redirects, you eliminate the possibility of the same page being accessible at both /about and /about.php

When implementing clean URLs, also ensure you set the correct canonical URL in your page headers and update your internal links to use the clean versions. For more on setting up proper headers, see our guide on configuring security headers.

Testing Your Configuration

After implementing clean URLs, run through this checklist:

# 1. Verify clean URLs work
curl -I https://example.com/about
# Expect: 200 OK

# 2. Verify old URLs redirect
curl -I https://example.com/about.php
# Expect: 301 -> /about

# 3. Verify no redirect loops
curl -v -L --max-redirs 3 https://example.com/about.php 2>&1 | grep -E "< HTTP|< Location"

# 4. Check trailing slash handling
curl -I https://example.com/about/
# Expect: Either 200 or 301 to /about (consistent with your policy)

# 5. Verify query strings are preserved
curl -I "https://example.com/search.php?q=test"
# Expect: 301 -> /search?q=test

# 6. Verify index files still work
curl -I https://example.com/
# Expect: 200 OK

# 7. Check that actual directories still work
curl -I https://example.com/images/
# Expect: 200 or 403 (not a redirect loop)

Summary

Removing file extensions from URLs creates cleaner, more professional, and more flexible URLs for your website. Apache's mod_rewrite handles this through .htaccess rules that redirect old extension-based URLs and internally rewrite clean URLs to the actual files. Nginx achieves the same result more efficiently with try_files and return 301 directives. The key to a successful implementation is using %{THE_REQUEST} in Apache to prevent redirect loops, handling trailing slashes consistently, preserving query strings, and verifying your redirects with curl -I. Proper 301 redirects ensure that search engines and existing links transition smoothly to your new clean URL structure. For additional server hardening, combine clean URLs with proper security headers and a well-configured Nginx or Apache setup.

Need help optimizing your web server configuration? Explore NOC.org plans to get started.

Improve Your Websites Speed and Security

14 days free trial. No credit card required.