skip to Main Content

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?

2

Answers


  1. Chosen as BEST ANSWER

    Based on @MrWhite his reponse the following .htaccess does what is expected

    Options -MultiViews
    
    DirectoryIndex index.php
    
    RewriteEngine On
    
    # Append trailing slash to non-assets
    RewriteCond %{REQUEST_URI} !(/$|.)
    RewriteRule . %{REQUEST_URI}/ [R=301,L]
    
    RewriteCond %{HTTP:Accept-Language} ^nl
    RewriteCond %{THE_REQUEST}  /+(?!(en|nl)/).*
    RewriteRule ^(.*)$ /nl/$1 [L,R=302]
    
    RewriteCond %{THE_REQUEST}  /+(?!(en|nl)/).*
    RewriteRule ^(.*)$ /en/$1 [L,R=302]
    
    # Abort early if request has already been rewritten OR maps to a file OR directory
    RewriteCond %{ENV:REDIRECT_STATUS} . [OR]
    RewriteCond %{REQUEST_FILENAME} -f [OR]
    RewriteCond %{REQUEST_FILENAME} -d
    RewriteRule ^ - [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]
    

  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search