Every web project eventually needs URL routing beyond simple page-to-page redirects. Whether you are enforcing a canonical domain, hiding file extensions for cleaner URLs, routing everything through a single entry point, or putting up a maintenance page during deployments, .htaccess provides battle-tested patterns for all of these scenarios.
This guide collects the most common routing patterns you will encounter in real-world projects. Each pattern is presented with a clear explanation, ready-to-use code, and notes on common mistakes. If you have already read through the fundamentals of redirects and URL rewrites, this is where you put that knowledge into practice.
WWW vs Non-WWW Canonicalization
Search engines treat www.example.com and example.com as two separate sites. If both versions serve the same content, you are creating duplicate content, which dilutes your SEO ranking and confuses crawlers. You must pick one version and redirect the other to it.
Redirecting WWW to Non-WWW
This is the most popular choice for modern websites. It produces shorter, cleaner URLs.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]
</IfModule>How it works:
RewriteCond %{HTTP_HOST} ^www\.(.+)$checks if the hostname starts withwww.and captures everything after it into%1.RewriteRule ^(.*)$matches any URL path and captures it into$1.- The substitution
https://%1/$1builds the new URL using the domain withoutwww.and the original path.
| Request | Redirects To |
|---|---|
http://www.example.com/about |
https://example.com/about |
https://www.example.com/blog/post |
https://example.com/blog/post |
https://example.com/about |
No redirect (already correct) |
Redirecting Non-WWW to WWW
Some organizations and larger sites prefer the www version. The www subdomain also offers more flexibility with DNS (for example, you can point a CNAME record at a CDN).
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteCond %{HTTP_HOST} !^localhost$ [NC]
RewriteCond %{SERVER_ADDR} !=127.0.0.1
RewriteCond %{SERVER_ADDR} !=::1
RewriteRule ^(.*)$ https://www.%{HTTP_HOST}/$1 [R=301,L]
</IfModule>The additional conditions for localhost, 127.0.0.1, and ::1 prevent the rule from firing during local development. Without them, your local environment would try to redirect to www.localhost, which does not exist.
Common Mistake: Protocol-Unaware Canonicalization
Wrong approach:
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]If your site does not have an SSL certificate, hardcoding https:// will cause a connection error. If you need to support both HTTP and HTTPS dynamically, detect the protocol first:
Correct approach (protocol-aware):
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} =on
RewriteRule ^ - [E=PROTO:https]
RewriteCond %{HTTPS} !=on
RewriteRule ^ - [E=PROTO:http]
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ %{ENV:PROTO}://%1/$1 [R=301,L]
</IfModule>The E=PROTO flag sets an environment variable that stores the current protocol, which is then used in the substitution. This way, HTTP requests stay on HTTP and HTTPS requests stay on HTTPS.
Domain Canonicalization
Sometimes you need to consolidate traffic from multiple domains onto a single primary domain. This is common after rebranding, acquiring competitor domains, or merging several sites into one.
Redirecting Multiple Domains to One
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_HOST} ^(www\.)?old-brand\.com$ [NC,OR]
RewriteCond %{HTTP_HOST} ^(www\.)?another-domain\.net$ [NC,OR]
RewriteCond %{HTTP_HOST} ^(www\.)?typo-domain\.com$ [NC]
RewriteRule ^(.*)$ https://primary-domain.com/$1 [R=301,L]
</IfModule>Each RewriteCond checks for one of the old domains. The [OR] flag means any one of them being true is enough for the rule to fire. The (www\.)? part makes each condition match both the www and non-www versions.
| Request | Redirects To |
|---|---|
http://old-brand.com/products |
https://primary-domain.com/products |
https://www.another-domain.net/about |
https://primary-domain.com/about |
https://primary-domain.com/about |
No redirect (already correct) |
Redirecting a Specific Path on a Different Domain
In some cases, only certain paths from an old domain need to be redirected to specific pages on the new domain:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_HOST} ^(www\.)?old-brand\.com$ [NC]
RewriteRule ^support/?$ https://primary-domain.com/help-center [R=301,L]
RewriteCond %{HTTP_HOST} ^(www\.)?old-brand\.com$ [NC]
RewriteRule ^(.*)$ https://primary-domain.com/$1 [R=301,L]
</IfModule>Clean URLs (Removing File Extensions)
Clean URLs without .html or .php extensions look more professional, are easier to remember, and are generally better for SEO. Instead of example.com/about.html, users see example.com/about.
Removing the .html Extension
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} \.html$ [NC]
RewriteRule ^(.+)\.html$ /$1 [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^(.+)$ $1.html [L]
</IfModule>This pattern works in two stages:
- External redirect: If someone visits
/about.html(or a search engine has the old URL indexed), they are 301-redirected to/about. This ensures only one URL exists for each page. - Internal rewrite: When someone visits
/about, Apache checks ifabout.htmlexists on disk. If it does, the request is internally rewritten to serve that file.
| Request URL | What Happens |
|---|---|
/about.html |
301 redirect to /about |
/about |
Internally serves /about.html (if file exists) |
/contact.html |
301 redirect to /contact |
/contact |
Internally serves /contact.html (if file exists) |
/style.css |
Served normally (no rewrite) |
Removing the .php Extension
The same pattern applies to PHP files:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} \.php$ [NC]
RewriteCond %{REQUEST_URI} !^/index\.php$ [NC]
RewriteRule ^(.+)\.php$ /$1 [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^(.+)$ $1.php [L]
</IfModule>Notice the additional condition !^/index\.php$ that excludes index.php. This is important if you are using a front controller pattern (discussed in the next section) where index.php is the main entry point. Without this exclusion, you could create a redirect loop.
Common Mistake: Forgetting the File Existence Check
Wrong approach:
RewriteRule ^(.+)$ $1.html [L]A request to /images/logo.png would be rewritten to /images/logo.png.html, which obviously does not exist and returns a 404 error.
Correct approach:
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^(.+)$ $1.html [L]The condition %{REQUEST_FILENAME}.html -f checks that the file with .html appended actually exists before performing the rewrite. Assets like images, CSS, and JavaScript files pass through untouched.
Front Controller Pattern
The front controller pattern routes all requests through a single entry point, typically index.php. This is the foundation of virtually every modern PHP framework, including WordPress, Laravel, Symfony, Slim, and many others.
Basic Front Controller
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]
</IfModule>This is deceptively simple but extremely important. Here is what happens for different requests:
| Request URL | File Exists? | What Happens |
|---|---|---|
/style.css |
Yes | Served directly (conditions fail) |
/images/logo.png |
Yes | Served directly (conditions fail) |
/about |
No | Rewritten to /index.php |
/users/42/profile |
No | Rewritten to /index.php |
The PHP application in index.php then reads $_SERVER['REQUEST_URI'] to determine which page or controller to load.
Front Controller with Query String Passthrough
Some frameworks expect the original URL to be passed as a query parameter:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php?url=$1 [QSA,L]
</IfModule>The QSA flag ensures that any existing query string parameters are appended. So a request to /products/search?category=electronics becomes /index.php?url=products/search&category=electronics.
Excluding Specific Directories
Sometimes you need to exclude certain directories from the front controller, for example, an admin panel or an API that has its own routing:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^(admin|api|assets)/ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]
</IfModule>The first rule matches paths starting with admin/, api/, or assets/ and uses - as the substitution (meaning "do nothing"), with the [L] flag to stop processing. These requests are passed through to Apache's normal file-serving behavior.
Language-Based Routing
Serving content in different languages based on the user's preferences is a common requirement for international sites. You can route users based on URL prefixes, the Accept-Language header, or cookies.
URL Prefix-Based Language Routing
The most SEO-friendly approach is to use language prefixes in URLs (/en/about, /fr/about, /de/about):
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^(en|fr|de|es)/(.*)$ /content/$1/$2 [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /content/en/$1 [L]
</IfModule>| Request URL | Internally Served From |
|---|---|
/en/about |
/content/en/about |
/fr/contact |
/content/fr/contact |
/about |
/content/en/about |
Auto-Detecting Language from Browser Headers
You can read the browser's Accept-Language header and redirect first-time visitors to the appropriate language version:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} ^/$ [NC]
RewriteCond %{HTTP:Accept-Language} ^fr [NC]
RewriteRule ^$ /fr/ [R=302,L]
RewriteCond %{REQUEST_URI} ^/$ [NC]
RewriteCond %{HTTP:Accept-Language} ^de [NC]
RewriteRule ^$ /de/ [R=302,L]
RewriteCond %{REQUEST_URI} ^/$ [NC]
RewriteRule ^$ /en/ [R=302,L]
</IfModule>Cookie-Based Language Persistence
After a user manually selects a language, you can store their preference in a cookie and use it for future visits:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{QUERY_STRING} lang=(en|fr|de|es)
RewriteRule ^(.*)$ /$1? [CO=user_lang:%1:.example.com:86400:/,R=302,L]
RewriteCond %{REQUEST_URI} ^/$ [NC]
RewriteCond %{HTTP_COOKIE} user_lang=(en|fr|de|es)
RewriteRule ^$ /%1/ [R=302,L]
</IfModule>The CO flag sets a cookie named user_lang with the matched language code, valid for 86400 seconds (one day), scoped to .example.com.
Query String-Based Rewrites
Sometimes you need to rewrite or redirect based on query string parameters. This is common when migrating from an old CMS that used query parameters for routing to a new system with clean URLs.
Redirecting Query String URLs to Clean URLs
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{QUERY_STRING} ^page=([a-zA-Z0-9_-]+)$ [NC]
RewriteRule ^index\.php$ /%1? [R=301,L]
</IfModule>Key details:
%{QUERY_STRING}does not include the leading?.- The capture group in the condition (
([a-zA-Z0-9_-]+)) is referenced as%1in the substitution. - The trailing
?in the substitution strips the original query string. Without it, the redirect would keep the query string and you would end up with/about?page=about.
| Request URL | Redirects To |
|---|---|
/index.php?page=about |
/about |
/index.php?page=contact |
/contact |
/index.php |
No redirect |
Rewriting with Multiple Query Parameters
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{QUERY_STRING} ^category=([^&]+)&id=([0-9]+)$ [NC]
RewriteRule ^products\.php$ /products/%1/%2? [R=301,L]
</IfModule>%1captures the category value.%2captures the id value.- The trailing
?strips the old query string from the redirected URL.
Common Mistake: Forgetting to Strip the Query String
Wrong approach:
RewriteCond %{QUERY_STRING} ^page=([a-zA-Z0-9_-]+)$
RewriteRule ^index\.php$ /%1 [R=301,L]A request to /index.php?page=about redirects to /about?page=about because Apache carries the original query string by default.
Correct approach:
RewriteCond %{QUERY_STRING} ^page=([a-zA-Z0-9_-]+)$
RewriteRule ^index\.php$ /%1? [R=301,L]Now /index.php?page=about correctly redirects to /about with no query string.
Maintenance Mode Page
During deployments, migrations, or emergency fixes, you may need to temporarily show a maintenance page to all visitors while allowing developers or specific IPs to access the live site.
Basic Maintenance Mode
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/maintenance\.html$ [NC]
RewriteCond %{REQUEST_URI} !^/assets/ [NC]
RewriteRule ^(.*)$ /maintenance.html [R=503,L]
</IfModule>
ErrorDocument 503 /maintenance.html
Header Set Retry-After "3600"Important details:
- The first condition
!^/maintenance\.html$prevents an infinite redirect loop. Without it, the maintenance page itself would be redirected. - The second condition
!^/assets/allows CSS, JavaScript, and images in the/assets/directory to load so the maintenance page looks presentable. - The
R=503flag returns a 503 Service Unavailable status, which tells search engines the downtime is temporary and they should come back later. - The
Retry-Afterheader suggests to crawlers how long to wait before retrying (in seconds).
Maintenance Mode with IP Whitelist
Allow specific IP addresses (your office, VPN, or home connection) to bypass the maintenance page:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REMOTE_ADDR} !^192\.168\.1\.100$
RewriteCond %{REMOTE_ADDR} !^10\.0\.0\.50$
RewriteCond %{REQUEST_URI} !^/maintenance\.html$ [NC]
RewriteCond %{REQUEST_URI} !^/assets/ [NC]
RewriteRule ^(.*)$ /maintenance.html [R=503,L]
</IfModule>
ErrorDocument 503 /maintenance.html
Header Set Retry-After "3600"All conditions are joined with the default AND logic. So the rule fires only when:
- The visitor's IP is NOT
192.168.1.100AND - The visitor's IP is NOT
10.0.0.50AND - The request is NOT for the maintenance page itself AND
- The request is NOT for assets.
Developers coming from the whitelisted IPs see the live site normally.
Maintenance Mode with Cookie-Based Bypass
An alternative approach uses a secret URL parameter to set a bypass cookie. This is useful when developers need access from varying IP addresses:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{QUERY_STRING} bypass=secretkey123
RewriteRule ^(.*)$ /$1? [CO=maintenance_bypass:1:.example.com:3600:/,L]
RewriteCond %{HTTP_COOKIE} maintenance_bypass=1
RewriteRule ^ - [L]
RewriteCond %{REQUEST_URI} !^/maintenance\.html$ [NC]
RewriteCond %{REQUEST_URI} !^/assets/ [NC]
RewriteRule ^(.*)$ /maintenance.html [R=503,L]
</IfModule>
ErrorDocument 503 /maintenance.html
Header Set Retry-After "3600"A developer visits https://example.com/?bypass=secretkey123, receives a cookie, and can then browse the site normally for one hour. Everyone else sees the maintenance page.
Common Mistake: Maintenance Redirect Loop
Wrong approach:
RewriteRule ^(.*)$ /maintenance.html [R=503,L]This creates an infinite loop: the request for /maintenance.html itself matches ^(.*)$ and gets redirected to /maintenance.html again, which matches again, and so on until the server gives up with a 500 error.
Correct approach:
RewriteCond %{REQUEST_URI} !^/maintenance\.html$ [NC]
RewriteRule ^(.*)$ /maintenance.html [R=503,L]Always exclude the maintenance page URL and any assets it depends on from the rewrite rule.
Quick Reference
Here is a summary of all routing patterns covered in this guide:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]
RewriteCond %{HTTP_HOST} ^(www\.)?old-domain\.com$ [NC]
RewriteRule ^(.*)$ https://new-domain.com/$1 [R=301,L]
RewriteCond %{REQUEST_URI} \.php$ [NC]
RewriteRule ^(.+)\.php$ /$1 [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^(.+)$ $1.php [L]
RewriteCond %{QUERY_STRING} ^page=([a-zA-Z0-9_-]+)$
RewriteRule ^index\.php$ /%1? [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]
</IfModule>Each pattern in this guide addresses a real-world problem you will encounter when building and maintaining websites. Start with the patterns you need immediately, test them thoroughly in a staging environment, and keep this reference handy for when new requirements arise. Correct routing is not just a technical detail; it directly impacts your site's user experience, SEO performance, and overall reliability.