I would like to ‘redirect’ (I’m not sure this is the correct technical term here) visitors on my website that go to domain/subfolder
to domain/subfolder/index.php
– but without index.php
shown in the browser's address-line field. I am already doing successful rewrites for all .php
page files in my root folder (domain/about
serves domain/about.php
), but I cannot get it to work for any subfolders. In the case of subfolders, there is a seemingly infinite loop of 'index' rewriting going on, and other pages in the subfolder cannot be accessed without the .php
ending added in the address.
I have worked on this with the help of online tools and ChatGPT but no version I have come up with actually works in my case. ChatGPT is convinced it should work, and all the online .htaccess
checking/testing tools show me the correct end result, but on the actual website, it does not work.
For example, when I go to domain/admin
, I do get served domain/admin/index.php
but the address field in the browser (and all relevant variables in debugging) show domain/admin/index.php/index/index/index/index/index/index/index/index/index/index/index/index/index/index/index/index/index/index
.
Also, when I try to access a different page in a subfolder (e.g., domain/admin/meta.php
), I can only get to the page when I use the .php
ending in the address, not via domain/admin/meta
. However, all pages in the root directy work fine. For example, domain/about
correctly serves domain/about.php
(with only domain/about
in the address field shown).
I have some other rewrites in the .htaccess
file to deal with language-related stuff. I don't think that's the issue but I know too little about it all to be 100% sure about that. Here's my full .htaccess
file:
Options -Indexes
RewriteEngine On
# Language switch for root with /en and /de
RewriteRule ^en/?$ index.php?lang=en [L]
RewriteRule ^de/?$ index.php?lang=de [L]
# Remove trailing slashes (except for root "/")
RewriteCond %{REQUEST_URI} !^/$
RewriteCond %{REQUEST_URI} ^(.+)/$
RewriteRule ^ %1 [R=301,L]
# Serve index.php for any folder without changing the URL
RewriteCond %{REQUEST_FILENAME} -d
RewriteCond %{REQUEST_FILENAME}/index.php -f
RewriteRule ^(.+)$ $1/index.php [L]
# Prevent infinite loops on index.php
RewriteCond %{THE_REQUEST} /index\.php [NC]
RewriteRule ^(.*)/index\.php$ /$1 [R=301,L]
# Ensure PHP-less URLs work (e.g., /about → /about.php)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([^/.]+)$ $1.php [L]
# Handle language-prefixed URLs (e.g., /en/about → about.php?lang=en)
RewriteRule ^(en|de)/([^/.]+)$ $2.php?lang=$1 [L]
# Client errors (4xx)
ErrorDocument 400 /error.php
ErrorDocument 401 /error.php
ErrorDocument 403 /error.php
ErrorDocument 404 /error.php
ErrorDocument 405 /error.php
ErrorDocument 408 /error.php
ErrorDocument 410 /error.php
ErrorDocument 413 /error.php
ErrorDocument 414 /error.php
ErrorDocument 429 /error.php
# Server errors (5xx)
ErrorDocument 500 /error.php
ErrorDocument 502 /error.php
ErrorDocument 503 /error.php
ErrorDocument 504 /error.php
ErrorDocument 505 /error.php
# Various security measures
<IfModule mod_headers.c>
Header set X-XSS-Protection "1; mode=block"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-Content-Type-Options "nosniff"
Header set Referrer-Policy "same-origin"
</IfModule>
I have tried many different versions of this, including rewriting for specific subfolders (such as /admin
) and doing what I can to prevent that index/index/index…
loop, but nothing has worked so far.
I’d be very grateful for any help. Thank you.
To serve index.php
(the "directory index") from a directory when requesting that directory is handled by mod_dir and the DirectoryIndex
directive. You shouldn't be trying to use mod_rewrite to do this. This defaults to index.html
, but often includes index.php
as well in many distributions of Apache.
Note that you should be requesting example.com/subfolder/
(with a trailing slash), not example.com/subfolder
as you have written. If you omit the trailing slash on a URL that maps to a physical directory then mod_dir will (by default) append the trailing slash with an external 301 redirect (resulting in 2 requests).
(Aside: What you are trying to do is internally "rewrite" the request. ie. No change in the URL in the browser's address bar. Although this is sometimes referred to as an "internal redirect" in the Apache docs. A "redirect" generally refers to an external HTTP redirect.)
To set index.php
as the directory index document throughout the directory tree you simply use the following near the top of the root .htaccess
file.
DirectoryIndex index.php
However, there are other errors with your .htaccess
file that are causing the rewrite/redirect loop you are seeing. And the inability to serve .php
files from subdirectories.
Your directives are in the wrong order. Generally, you should have external redirects before internal rewrites, otherwise, you can end up redirecting to URLs that you have internally rewritten to (like your language rewrites at the top) and exposing your internal URLs (losing the information in the originally requested URL).
# Remove trailing slashes (except for root "/") RewriteCond %{REQUEST_URI} !^/$ RewriteCond %{REQUEST_URI} ^(.+)/$ RewriteRule ^ %1 [R=301,L]
You can't blindly remove trailing slashes from the URL like this. Since you can't remove the trailing slash after a physical filesystem directory. As mentioned above, your URLs that request directories should end in a trailing slash.
Yes, you can have URLs for directories that do not end in a slash (using the DirectorySlash Off
directive), but you then need to internally rewrite the request to append the trailing slash - which gets messy. At the end of the day, Apache needs the trailing slash on the end of the URL when serving directory index documents from that directory.
A minor thing, but the first condition in the above rule is superfluous. The second condition could only be successful when the first condition is also successful. (The first condition would only be required if you had used the *
quantifier in the 2nd condition/regex.)
# Serve index.php for any folder without changing the URL RewriteCond %{REQUEST_FILENAME} -d RewriteCond %{REQUEST_FILENAME}/index.php -f RewriteRule ^(.+)$ $1/index.php [L]
This isn't required. Use DirectoryIndex index.php
instead as mentioned above.
This is also contributing to your rewrite loop since REQUEST_FILENAME
is not necessarily what you think it is. This can result in rewriting to a different URL than you are expecting.
# Prevent infinite loops on index.php RewriteCond %{THE_REQUEST} /index\.php [NC] RewriteRule ^(.*)/index\.php$ /$1 [R=301,L]
This isn't about "preventing infinite loops" but removing index.php
should it be in the requested URL. (Although checking against THE_REQUEST
is attempting to prevent the infinite loop.)
This also doesn't handle requests for index.php
in the root directory, only for subdirectories.
# Ensure PHP-less URLs work (e.g., /about → /about.php) RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^([^/.]+)$ $1.php [L]
The regex ^([^/.]+)$
only checks for URLs in the root directory, not subdirectories since you exclude /
from the captured pattern.
You need to check that the target file exists before rewriting to the corresponding .php
file. No need to check that the request does not map to a directory or file.
# Handle language-prefixed URLs (e.g., /en/about → about.php?lang=en) RewriteRule ^(en|de)/([^/.]+)$ $2.php?lang=$1 [L]
You also need to check that the target .php
file exists before rewriting, otherwise /en/anything
is going to be rewritten to anything.php?lang=en
. (And that's the URL that will appear in your server's access logs, not /en/anything
that the user requested.)
Like the rule above, this also only handles URLs of the form /<lang-code>/<path-segment>
(in your root directory), not /<lang-code>/<dir>/<file>
as I assume it should?
# Language switch for root with /en and /de RewriteRule ^en/?$ index.php?lang=en [L] RewriteRule ^de/?$ index.php?lang=de [L]
These can be combined into a single rule. However, these rules allow both /en
and /en/
(two different URLs) to serve the same content (potentially a "duplicate content" issue). Only one (with or without the trailing slash) should serve the content. I'm assuming with a trailing slash. Consequently, you need an additional rule to append the trailing slash to these URLs if omitted.
Taking the above points into consideration, try the following instead:
(This assumes that URLs for directories and "/en/" etc. end in a trailing slash.)
Options -Indexes
# Allow "index.php" to be served from that directory when requesting a directory
DirectoryIndex index.php
RewriteEngine On
# Redirect to append trailing slash if omitted from root language URL
# eg. "/en" to "/en/"
RewriteRule ^(en|de)$ /$1/ [R=301,L]
# Remove trailing slashes (except for physical dirs and language code prefixes)
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond $1 !^(en|de)$
RewriteRule ^(.+)/$ /$1 [R=301,L]
# Remove "index.php" if present in the requested URL (not the rewritten URL)
# Any directory, including the document root
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteCond %{REQUEST_URI} ^(.*/)index\.php$
RewriteRule (^|/)index\.php$ %1 [R=301,L]
# Language switch for root with /en/ and /de/
RewriteRule ^(en|de)/$ index.php?lang=$1 [L]
# Handle language-prefixed URLs (e.g., /en/about → about.php?lang=en)
# For any URL-path depth
RewriteCond %{DOCUMENT_ROOT}/$2.php -f
RewriteRule ^(en|de)/([^.]+)$ $2.php?lang=$1 [L]
# Ensure PHP-less URLs work (e.g., /about → /about.php) for any directory
RewriteCond %{DOCUMENT_ROOT}/$1.php -f
RewriteRule ^([^.]+)$ $1.php [L]
# Client errors (4xx)
: etc.
Make sure the browser cache is cleared before testing and test first with 302 (temporary) redirects to avoid potential caching issues. 301s are cached persistently by the browser - so if you make an error then the error is also cached, which makes testing problematic.
Additional notes...
The REDIRECT_STATUS
environment variable is used to prevent a rewrite loop (similar to using THE_REQUEST
, but cleaner IMO and less prone to error). The REDIRECT_STATUS
env var is empty on the initial request and set to 200
(as in 200 OK HTTP status) after the first successful rewrite (or an error code).
These rules do assume you are not using the query string (other than for the lang
URL param). If a query string was present on a URL that includes a language code prefix then it would effectively be removed.
You should probably implement canonical redirects for URLs that include the .php
prefix (that map to physical files) and URLs that include the lang
URL param but omit the language prefix on the URL-path.
all the online .htaccess checking/testing tools show me the correct end result
The online testers are clearly wrong in this case. A couple of issues with the online testers I've seen is they have no knowledge of your filesystem and only perform a single pass through the rules (so are unable to detect rewrite/redirect loops). eg. A request for example.com/foo
will be handled differently by the webserver if /foo
is a directory, a file or does not exist.