Mod_rewrite: rewrite existing file to slug, slug to existing file

.htaccessapache-2.2mod-rewrite

My situation is as follows: I have static files in folder public/ on my local server. Any request that comes in should be forwarded to that folder, but if a file in that folder is accessed directly, it should be either a 404 or redirect to the 'slug'. Example:

Request: /my-file
Should rewrite to: /public/my-file.html
URL should be: http://localhost/my-file

Request: /public/my-file.html
Should rewrite to: /public/my-file.html
URL should be: http://localhost/my-file

I have the following rules and conditions in my .htaccess:

RewriteCond %{DOCUMENT_ROOT}/public/%{REQUEST_URI}.html -f
RewriteRule ^.*$ /public/$0.html [L,C]
RewriteCond %{REQUEST_URI} ^/public
RewriteRule ^public/(.+).html$ /$1 [R=301,L]

From top to bottom:

  1. Is /public/{request}.html a file?
  2. Rewrite if (1) is true: /public/{request}.html, and chain to (3) to prevent a redirect loop
  3. Does the original request start with /public?
  4. Rewrite if (3) is true, and original request matches public/{request}.html: /{request}. Also make a 301 header, which tells my browser and crawlers the page has moved permanently. The request will run through all the rules again

So, if we do those two requests:

1. Request: /my-file
  1. /public/my-file.html evaluates to true; the file exists in my folder.
  2. Rewrites to: /public/my-file.html, chain to 3
  3. The original request does not match.
  4. Skipped

So far, so good; the right file is shown with the URL I want. Request number 2, which is a request for the actual file as it exists in the file structure:

2. Request: /public/my-file.html
  1. public/public/my-file.html.html evaluates to false, the file does not exist in my folder.
  2. Skipped
  3. The original request matches: it starts with /public
  4. Rewrite public/my-file.html to /my-file, and go from the top.

Now, after (4) in the 2nd example, I'd say that the request matches the 1st example, meaning I get the page I'm looking for with the right URL (because of the 301). But it doesn't work: the URL doesn't change, I'm getting the page I'm looking for but not the URL (which should be /my-file).

Does anyone have pointers? I think I'm almost there but doing something wrong with the chaining or redirect. Thanks!

Edit

After having tried all kinds of stuff, I finally decided that what I want is probably not possible. I want to redirect an existing file (public/my-file.html) to a non-existent file (my-file) visually, but internally I want it to request the existing file path. This will invariably result in a redirect loop, because mod_rewrite just doesn't work that way:

User typed: website.com/my-file
Rewrites to file path: .../wwwroot/public/my-file.html
Internal redirect
Rewriterule found: /public/my-file.html should redirect to my-file
See above.

I haven't come across any comparable situation here on ServerFault or on StackOverflow – usually the rewriting goes to a generic .php file, not to an existing .html file.

I marked Krist van Besien's answer as the right one since it pointed me in the right direction for figuring all this out.

Edit 2

In a followup question, the problem was solved by using %{THE_REQUEST}: See here

Best Answer

That it doesn't work as expected has to do with the order RewriteConds and RewriteRules are executed. RewriteRules are evaluated before the RewriteConds that precede them. So the order your request for /public/my-file.htmlis processed is like this: (I'll keep your numbering)

  • First the URL gets checked against the test pattern of 2. And this matches..* matches anything after all..
  • Then the RewriteCond 1 gets evaluated. Since this fails the subsitution in 2 is not performed.
  • The the URI gets checked against the test pattern of 4. This test pattern does not match, as your URI startes with /. The rule fails, so the RewriteCond at 3 never gets evaluated.

RewriteConds (and you can have several) belong to the RewriteRule that follows them, and only get evaluated if the pattern in the RewriteCond matches first. This is a bit counterintuitive, I know.

Read about the syntax here: Redirect, Change URLs or Redirect HTTP to HTTPS in Apache - Everything You Ever Wanted to Know About Mod_Rewrite Rules but Were Afraid to Ask

How to solve your problem?

Maybe something like:

RewriteRule  ^/public/(.*)$     /$1 [R=301,L]
RewriteRule  ^(-*)$             /public/$1

And a last tip: Enable the Rewrite Log. You'll learn a bundle...