Skip to content

Common Routing Patterns in .htaccess

Learn battle-tested .htaccess patterns for canonicalization, clean URLs, front controllers, language routing, query string rewrites, and maintenance mode with ready-to-use code and best practices.

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.

www to non-wwwapache
<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 with www. and captures everything after it into %1.
  • RewriteRule ^(.*)$ matches any URL path and captures it into $1.
  • The substitution https://%1/$1 builds the new URL using the domain without www. 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).

non-www to wwwapache
<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

Multiple domains to a primary domainapache
<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

Clean URLs without .htmlapache
<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:

  1. 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.
  2. Internal rewrite: When someone visits /about, Apache checks if about.html exists 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:

Clean URLs without .phpapache
<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

Front controller patternapache
<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:

Front controller with query stringapache
<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:

Front controller with exclusionsapache
<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):

Language prefix routingapache
<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:

Auto-detect language and redirectapache
<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>

After a user manually selects a language, you can store their preference in a cookie and use it for future visits:

Cookie-based language routingapache
<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

Query string to clean URL redirectapache
<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 %1 in 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

Multiple query parametersapache
<IfModule mod_rewrite.c>
  RewriteEngine On

  RewriteCond %{QUERY_STRING} ^category=([^&]+)&id=([0-9]+)$ [NC]
  RewriteRule ^products\.php$ /products/%1/%2? [R=301,L]
</IfModule>
  • %1 captures the category value.
  • %2 captures 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

Simple maintenance modeapache
<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=503 flag returns a 503 Service Unavailable status, which tells search engines the downtime is temporary and they should come back later.
  • The Retry-After header 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:

Maintenance mode with IP whitelistapache
<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.100 AND
  • The visitor's IP is NOT 10.0.0.50 AND
  • 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.

An alternative approach uses a secret URL parameter to set a bypass cookie. This is useful when developers need access from varying IP addresses:

Maintenance mode with cookie bypassapache
<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:

Common Routing Patterns Cheat Sheetapache
<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.

Navigation