I have a small PHP website with 3 pages. The page content is dynamically translated in Dutch or English (I take the language from the URL)
\index.php
\page-one.php
\page-two.php
I want to achieve the following URL's
https://www.example.com/ => https://www.example.com/en/ or nl/ depending browser language
https://www.example.com/en/ => \index.php
https://www.example.com/en/page-one/ => \page-one.php
https://www.example.com/en/page-two/ => \page-two.php
https://www.example.com/nl/ => \index.php
https://www.example.com/nl/page-one/ => \page-one.php
https://www.example.com/nl/page-two/ => \page-two.php
It works locally on my PC with WAMP with the following htaccess
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_URI} !(/$|\.)
RewriteRule (.*) %{REQUEST_URI}/ [R=301,L]
RewriteCond %{HTTP:Accept-Language} ^nl
RewriteCond %{THE_REQUEST} \ /+(?!(en|nl)/).*
RewriteRule ^(.*)$ /nl/$1 [L,R]
RewriteRule ^nl/(.*)$ /$1 [L]
RewriteCond %{THE_REQUEST} \ /+(?!(en|nl)/).*
RewriteRule ^(.*)$ /en/$1 [L,R]
RewriteRule ^en/(.*)$ /$1 [L]
However, when I publish it on the shared webhosting (at OVH) the sub-folder with the page name points to the index file
OK https://www.example.com/ => https://www.example.com/en/ or nl/
OK https://www.example.com/en/ => \index.php
NOK https://www.example.com/en/page-one/ => \index.php
NOK https://www.example.com/en/page-two/ => \index.php
same for the /nl/
The pages only show as follow
https://www.example.com/en/page-one/page-one/ => \page-one.php
https://www.example.com/en/page-two/page-two/ => \page-two.php
But also these URL's works, which should not be the case
https://www.example.com/en/page-one/page-two/ => \page-two.php
https://www.example.com/en/page-two/page-one/ => \page-one.php
It seems that it runs line 10 and 14 of the htaccess twice.
How can I solve this?
This looks like a conflict with MultiViews
(part of mod_negotiation). This would explain how this is able to work at all locally and the difference in behaviour on the live server (where I suspect MultiViews is not enabled). (Although it does not seem to explain how /en/page-one/page-one/
seemingly "works" on the live server? This would, however, work locally with MultiViews enabled. The same applies to /en/page-one/page-two/
- more on that below.)
In your mod_rewrite directives you are not appending the .php
extension at any point, so by themselves they cannot possibly work (unless you are requesting page-one.php
- with the .php
extension - directly). So, it looks like you are relying on MultiViews (which effectively appends the file extension).
But also these URL's works, which should not be the case
https://www.example.com/en/page-one/page-two/ => \page-two.php https://www.example.com/en/page-two/page-one/ => \page-one.php
It is MultiViews that allows something like this to "work". Although I would expect this to be the other way round. ie. /en/page-one/page-two/
would serve /page-one.php
, not page-two.php
as you suggest?
What happens here is that your mod_rewrite rule internally rewrites a request for /en/page-one/page-two/
to /page-one/page-two/
. MultiViews then initiates an internal subrequest to /page-one.php/page-two/
(/page-two/
is simply PATH-INFO) and /page-one.php
is served.
You need to ensure that MultiViews is disabled. And then manually append the .php
extension where appropriate. However, you've not stated how you are managing your static assets (JS, CSS, images, etc.)? Should these be subject to the same redirect/rewrites? Are these language specific also?
I would assume you are linking directly to the static assets, so these should not be subject to URL-rewriting.
Try something like the following instead:
# Ensure that MultiViews is disabled
Options -MultiViews
DirectoryIndex index.php
RewriteEngine On
# Abort early if request has already been rewritten
RewriteCond %{ENV:REDIRECT_STATUS} .
RewriteRule ^ - [L]
# Abort early if request maps to a file OR directory (except root)
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule . - [L]
# Append trailing slash to non-assets
RewriteCond %{REQUEST_URI} !(/$|\.)
RewriteRule . %{REQUEST_URI}/ [R=301,L]
# Prefix request with language code if omitted (302 - temporary)
# (Defaults to "en" if not "nl" or omitted)
RewriteCond %{HTTP:Accept-Language}@en (?:^|@)(nl|en)
RewriteRule !^(en|nl)/ /%1%{REQUEST_URI} [R=302,L]
# Rewrite to remove language prefix and append ".php" extension if file exists
RewriteCond %{DOCUMENT_ROOT}/$1.php -f
RewriteRule ^(?:en|nl)/([^/]+)/$ $1.php [L]
# Otherwise, rewrite to remove the language prefix (handles the DirectoryIndex)
RewriteRule ^(?:en|nl)/(.*) $1 [L]
The RewriteBase
directive in your original rule block was not required (and it's not required here either).
No need to check THE_REQUEST
since we are aborting early when the request has already been internally rewritten (by checking against the REDIRECT_STATUS
environment variable - which is empty on the initial request and set the to HTTP status after the first successful rewrite).
If on Apache 2.4 then you can use the END
flag, instead of L
, on the last two rules (both rewrites) and remove the first "Abort early" rule that checks the REDIRECT_STATUS
env var. The END
flag stops all processing so no "loop" occurs.
Further improvements... consider redirecting the request if index.php
, page-one.php
or page-two.php
(ie. anything with a .php
extension) are requested directly.