1

I'm removing the .php extension from files contained within a specific directory on my website (courses) via .htaccess on Apache/2.2.26 (Unix).

Additionally, I'd like to 301 redirect the old .php version to non-php version.

Old URL Structure:

http://www.example.com/courses/blue-course.php

New URL Structure:

http://www.example.com/courses/blue-course

My Current Issues:

  • non .php version of the pages are not 301 redirecting.
  • both .php and non .php versions of the pages are viewable.

Here's my code:

RewriteEngine On

# External Routing
RewriteCond %{REQUEST_URI} (.*\/courses\/)([^\s]+)\.php [NC]
RewriteRule (.*courses\/)([^\s]+)\.php http://www.example.com/$1$2 [L,R=301]

# Internal Routing
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^ %{REQUEST_URI}.php [L]
0

Rather than "not 301 redirecting" at all, I would expect your code to create a redirect loop. As the earlier redirect will also catch the rewritten URL (by the later directive) and redirect, etc. etc.

non .php version of the pages are not 301 redirecting.

Isn't that correct? Or should the "non" or "not" be removed here?

both .php and non .php versions of the pages are viewable.

That would seem to suggest that perhaps MultiViews is enabled. MultiViews needs to be disabled for your mod_rewrite directives to execute correctly. (You either use MultiViews or mod_rewrite here, however, you may have issues trying to redirect if MultiViews is enabled.) To ensure MultiViews is disabled, add the following at the top of your .htaccess file:

Options -MultiViews

However, to avoid a redirect loop, instead of matching against REQUEST_URI, which changes as the URL is rewritten (the same as the URL-path that is matched by the RewriteRule pattern), you should either match against THE_REQUEST (which holds the first line of the request and does not change) OR simply check that the REDIRECT_STATUS environment variable is empty to ensure you are only checking the initial request and not the rewritten request.

For example:

# External Routing
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule (.*courses\/)([^\s]+)\.php http://www.example.com/$1$2 [L,R=301]

NB: Always test with 302 (temporary) redirects to avoid caching issues.

However, your regex could be cleaned up a bit. According to your example URLs, /courses is the first path segment, however, your regex is matching "courses/" anywhere in the URL-path. And you are allowing any URL with path-info to be redirected - is that intentional? You only need a single backreference; not two. And there's no need to escape slashes in the RewriteRule pattern. You don't necessarily need the scheme+hostname in the substitution string unless you have multiple domains?

So, this could perhaps be "simplified" to:

# External Routing
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule ^(courses/[^\s]+)\.php$ /$1 [L,R=301]

# Internal Routing
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^ %{REQUEST_URI}.php [L]

This may work for the specific URLs you are testing, however, this rule can break easily (and it's not specifically targeting the /courses subdirectory). %{REQUEST_FILENAME}.php is not necessarily the same as %{REQUEST_URI}.php. So, whilst the condition might succeed, you can still end up rewriting to an invalid URL, which could result in an endless loop - 500 Internal Server Error.

For example, given a request for /courses/blue-course/foo (based on your example), where /courses is a directory on the filesystem and blue-course.php is the file you intend to rewrite to (there is no subdirectory /blue-course) and /foo is just something that has been mistakenly added to the URL (perhaps even maliciously by an external third party), then this will result in a 500 error. Because %{REQUEST_FILENAME}.php resolves to <document-root>/courses/blue-course.php (which exists) but %{REQUEST_URI}.php resolves to /courses/blue-course/foo.php (which does not exist). The rewriting process then starts over, resulting in an endless loop.

This can be resolved by adjusting the rule to something like:

# Internal Routing
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.php -f
RewriteRule ^courses/ %{REQUEST_URI}.php [L]

Assuming you don't have multiple file extensions, or dots in the file basename, eg. blue-course.abc.php then this can also be optimised by making sure that the requested URL does not already have a file extension (to avoid any static resources being tested). For example:

# Internal Routing
RewriteCond %{REQUEST_URI} !\.\w{2,4}$
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.php -f
RewriteRule ^courses/ %{REQUEST_URI}.php [L]

The ! prefix on the CondPattern negates its meaning.

In Summary

Options -MultiViews

RewriteEngine On

# External Routing
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule ^(courses/[^\s]+)\.php$ /$1 [L,R=301]

# Internal Routing
RewriteCond %{REQUEST_URI} !\.\w{2,4}$
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.php -f
RewriteRule ^ %{REQUEST_URI}.php [L]

The directives above assume you are using the .htaccess file in the document root of your site. However, if you only need to target a specific directory and have minimal other mod_rewrite directives you need to inherit from parent configs then it can be beneficial to create an additional .htaccess file in that subdirectory to keep the config separate. However, the directives would need to be altered slightly:

RewriteEngine On

# External Routing
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule ^([^\s]+)\.php$ /courses/$1 [L,R=301]

# Internal Routing
RewriteCond %{REQUEST_URI} !\.\w{2,4}$
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.php -f
RewriteRule ^ %{REQUEST_URI}.php [L]

Note that this would (by default) completely override any mod_rewrite directives in a parent config (directory context).

  • 1
    I sincerely appreciate you taking the time to submit such an informative (and correctly functioning) response. To clarify having a separate .htaccess file in the directory: Will it essentially override all rules in parent config that are applicable only to that specific directory? – ju1985oh Jun 11 at 0:30
  • With a separate .htaccess file in the subdirectory... By default, all the mod_rewrite directives in the parent .htaccess file will be overridden (they will simply not execute) when a /courses/<whatever> URL is requested. However, other modules (such as mod_alias, Redirect or mod_setenvif, SetEnvIf or mod_expires, etc.) will execute as normal. Each Apache module works independently. By default, mod_rewrite directives are not inherited. There are ways to enable mod_rewrite inheritance (although on Apache 2.2 you are very limited in this respect), but this is not necessarily trivial. – MrWhite Jun 11 at 1:23

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.