13

I have a URL which may contain three parameters:

  1. ?category=computers
  2. &subcategory=laptops
  3. &product=dell-inspiron-15

I need 301 redirect this URL to its friendly version:

http://store.example.com/computers/laptops/dell-inspiron-15/

I have this but cannot make it to work if the query string parameters are in any other order:

RewriteCond %{QUERY_STRING} ^category=(\w+)&subcategory=(\w+)&product=(\w+) [NC]
RewriteRule ^index\.php$ http://store.example.com/%1/%2/%3/? [R,L]
TrueBlue
  • 480
  • 1
  • 4
  • 9
  • Most easy solution is to have PHP do the redirect. – Gerben Jan 13 '13 at 19:50
  • @Gerben that's assuming one is using PHP ... the question was specific to apache, which could be used with any number of languages/frameworks/etc. True, using PHP (or any other scripting or programming language) to manipulate URL parameters has some advantages, but this question was specifically about apache/mod_rewrite. – Aaron Wallentine Feb 28 '18 at 23:51
  • 1
    In this specific case the OP seemed to be using PHP, as the RewriteRule contained index.php. – Gerben Mar 01 '18 at 08:56
  • https://stackoverflow.com/questions/69290934/apache-rewrites-for-unknown-query-parameters - Can any one help on this similar issue – vssrnr Oct 28 '21 at 05:49

3 Answers3

18

You can achieve this with multiple steps, by detecting one parameter and then forwarding to the next step and then redirecting to the final destination

RewriteEngine On

RewriteCond %{QUERY_STRING} ^category=([^&]+) [NC,OR]
RewriteCond %{QUERY_STRING} &category=([^&]+) [NC]
RewriteRule ^index\.php$ $0/%1

RewriteCond %{QUERY_STRING} ^subcategory=([^&]+) [NC,OR]
RewriteCond %{QUERY_STRING} &subcategory=([^&]+) [NC]
RewriteRule ^index\.php/[^/]+$ $0/%1

RewriteCond %{QUERY_STRING} ^product=([^&]+) [NC,OR]
RewriteCond %{QUERY_STRING} &product=([^&]+) [NC]
RewriteRule ^index\.php/([^/]+/[^/]+)$ http://store.example.com/$1/%1/? [R,L]

To avoid the OR and double condition, you can use

RewriteCond %{QUERY_STRING} (?:^|&)category=([^&]+) [NC]

as @TrueBlue suggested.

Another approach is to prefix the TestString QUERY_STRING with an ampersand &, and check always

RewriteCond &%{QUERY_STRING} &category=([^&]+) [NC]

This technique (prefixing the TestString) can also be used to carry forward already found parameters to the next RewriteCond. This lets us simplify the three rules to just one

RewriteCond &%{QUERY_STRING} &category=([^&]+) [NC]
RewriteCond %1!&%{QUERY_STRING} (.+)!.*&subcategory=([^&]+) [NC]
RewriteCond %1/%2!&%{QUERY_STRING} (.+)!.*&product=([^&]+) [NC]
RewriteRule ^index\.php$ http://store.example.com/%1/%2/? [R,L]

The ! is only used to separate the already found and reordered parameters from the QUERY_STRING.

Olaf Dietsche
  • 72,253
  • 8
  • 102
  • 198
  • On first rule the url is changed to e.g. `/computers/`. Shouldn't next rule check for `/\w+/` instead of index.php? – Salman A Jan 13 '13 at 21:52
  • @SalmanA The first rule changes the URL to `index.php/computers`. So the next rule must check for `index.php/...`. I fixed the pattern to `index.php/[^/]+`. – Olaf Dietsche Jan 13 '13 at 21:58
  • I got it to work with minor tweaks (e.g. used `(?:^|&)` to eliminate the OR clause). – TrueBlue Jan 14 '13 at 07:52
  • The last solution seems to only work if all the parameters are included in the query. If one is missing it doesn't seem to work. Is there some easy fix for that? – CodeX May 11 '15 at 01:51
  • @CodeX All `RewriteCond`s for a rule must match, so this is not possible in one rule. If you just want to use the prefixing technique, this is possible with the first solution too. This means, you can take the first solution and replace the two `ReplaceCond`s with just one. – Olaf Dietsche Jun 15 '15 at 12:42
3

I take a slightly different approach for this sort of thing, leveraging ENV VARs set and read by mod_rewrite. I find it more readable / maintainable to refer to the backreferences by name like this, and these ENV VARs can be reused later in request processing too. Overall I think it's a more powerful and flexible approach than the accepted answer here. In any case, it works well for me. I've copied my gist below in its entirety:

From https://gist.github.com/cweekly/5ee064ddd551e1997d4c

# Mod_rewrite is great at manipulating HTTP requests.
# Using it to set and read temp env vars is a helpful technique.
#
# This example walks through fixing a query string:
# Extract good query params, discard unwanted ones, reorder good ones, append one new one.
#
# Before: /before?badparam=here&baz=w00t&foo=1&bar=good&mood=bad
# After: /after?foo=1&bar=good&baz=w00t&mood=happy
#
# Storing parts of the request (or anything you want to insert into it) in ENV VARs is convenient.
# Note the special RewriteRule target of "-" which means "no redirect; simply apply side effects"
# This lets you manipulate the request at will over multiple steps.
#
# In a RewriteRule, set custom temp ENV VARs via [E=NAME:value]
# Note it's also possible to set multiple env vars
# like [E=VAR_ONE:hi,E=VAR_TWO:bye]
#
# You can read these values using %{ENV:VAR_NAME}e <- little "e" is not a typo
#
# Tangent:
# Note you can also read these env vars the same way, if you set them via SetEnvIf[NoCase]
# (It won't work to use SetEnv, which runs too early for mod_rewrite to pair with it.)
#
# Regex details:
# (?:) syntax means "match but don't store group in %1 backreference"
#      so (?:^|&) is simply the ^ beginning or an & delimiter
#      (the only 2 possibilities for the start of a qs param)
# ([^&]+) means 1 or more chars that are not an & delimiter

RewriteCond %{QUERY_STRING} (?:^|&)foo=([^&]+)
RewriteRule ^/before - [E=FOO_VAL:%1]

RewriteCond %{QUERY_STRING} (?:^|&)bar=([^&]+)
RewriteRule ^/before - [E=BAR_VAL:%1]

RewriteCond %{QUERY_STRING} (?:^|&)baz=([^&]+)
RewriteRule ^/before - [E=BAZ_VAL:%1]

RewriteRule ^/before /after?foo=%{FOO_VAL}e&bar=%{BAR_VAL}e&baz=%{BAZ_VAL}e&mood=happy [R=301,L]

P.S. This is not a copy/pasteable solution to your question, but rather shows exactly how to handle this kind of problem. Armed w this understanding, leveraging it for your example will be completely trivial. :)

cweekly
  • 8,706
  • 1
  • 22
  • 17
  • "`using %{ENV:VAR_NAME}e <- little "e" is not a typo`" - With mod_rewrite, you don't use the trailing "e" syntax, unlike mod_headers (and other modules). But you'd never use both the `ENV:` prefix and trailing `e` together. Instead of `%{FOO_VAL}e` in the `RewriteRule` _substitution_, this should be `%{ENV:FOO_VAL}` - otherwise you'll just get a literal "e" in the result. – MrWhite Sep 19 '20 at 00:04
  • Note also that, as posted, these directives would only "work" in a _server_ or _virtualhost_ context, not `.htaccess`, due to the slash prefix on the `RewriteRule` pattern. You would also likely need to check that the query params were not already in the correct order to avoid an unnecessary redirect or potential redirect-loop. Although, in the above code this is "worked-around" by redirecting to a different URL-path - this is often not desirable. – MrWhite Sep 24 '20 at 00:45
  • Along similar lines... I've written [a more "generic" (drop in) solution](https://stackoverflow.com/a/64037881/369434) for [another question](https://stackoverflow.com/questions/63961200/rewritecond-to-reorder-query-parameters-to-predefined-order) with regards to re-ordering URL parameters and discarding empty or not-required params to create a canonical URL. – MrWhite Sep 24 '20 at 01:08
2

1) In case You just need to check that all parameters are in url:

   RewriteCond %{QUERY_STRING} (^|&)category\=computers($|&)
   RewriteCond %{QUERY_STRING} (^|&)subcategory\=laptops($|&)
   RewriteCond %{QUERY_STRING} (^|&)product\=dell\-inspiron\-15($|&)
   RewriteRule ^$ http://store.example.com/computers/laptops/dell-inspiron-15/? [R=301,L]

2) In case You need exact set of parameters:

 RewriteCond %{QUERY_STRING} ^&*(?:category\=computers|subcategory\=laptops|product\=dell\-inspiron\-15)(?!.*&\1(?:&|$))(?:&+(category\=computers|subcategory\=laptops|product\=dell\-inspiron\-15)(?!.*&\1(?:&|$))){2}&*$
 RewriteRule ^$ http://store.example.com/computers/laptops/dell-inspiron-15/? [R=301,L] 

This rule is generated by 301 redirect generator

Res Pro
  • 76
  • 3