Skip to main content

Common Routing Patterns in .htaccess

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-www
<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.
RequestRedirects To
http://www.example.com/abouthttps://example.com/about
https://www.example.com/blog/posthttps://example.com/blog/post
https://example.com/aboutNo 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 www
<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.

warning

Always use 301 (permanent) redirects for canonicalization. A 302 redirect tells search engines the original URL might come back, so they will not transfer ranking signals to the canonical version. Using 301 ensures search engines consolidate all authority on your chosen URL.

Common Mistake: Protocol-Unaware Canonicalization

Wrong approach:

# This redirects to https:// even if you don't have SSL configured
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

# Detect the current protocol
RewriteCond %{HTTPS} =on
RewriteRule ^ - [E=PROTO:https]
RewriteCond %{HTTPS} !=on
RewriteRule ^ - [E=PROTO:http]

# Redirect www to non-www using the detected protocol
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 domain
<IfModule mod_rewrite.c>
RewriteEngine On

# Redirect old domains to primary domain
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.

RequestRedirects To
http://old-brand.com/productshttps://primary-domain.com/products
https://www.another-domain.net/abouthttps://primary-domain.com/about
https://primary-domain.com/aboutNo 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>
tip

Place more specific rules before general ones. In the example above, the /support rule must come before the catch-all rule, otherwise it would never be reached.

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 .html
<IfModule mod_rewrite.c>
RewriteEngine On

# Redirect requests with .html to the clean version (external redirect)
RewriteCond %{REQUEST_URI} \.html$ [NC]
RewriteRule ^(.+)\.html$ /$1 [R=301,L]

# Internally rewrite clean URLs to the .html file
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 URLWhat Happens
/about.html301 redirect to /about
/aboutInternally serves /about.html (if file exists)
/contact.html301 redirect to /contact
/contactInternally serves /contact.html (if file exists)
/style.cssServed normally (no rewrite)

Removing the .php Extension

The same pattern applies to PHP files:

Clean URLs without .php
<IfModule mod_rewrite.c>
RewriteEngine On

# Redirect requests with .php to the clean version
RewriteCond %{REQUEST_URI} \.php$ [NC]
RewriteCond %{REQUEST_URI} !^/index\.php$ [NC]
RewriteRule ^(.+)\.php$ /$1 [R=301,L]

# Internally rewrite clean URLs to the .php file
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:

# This blindly appends .html to EVERY request
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:

# Only rewrite if the .html file actually exists
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 pattern
<IfModule mod_rewrite.c>
RewriteEngine On

# If the requested file exists, serve it directly
RewriteCond %{REQUEST_FILENAME} !-f
# If the requested directory exists, serve it directly
RewriteCond %{REQUEST_FILENAME} !-d
# Otherwise, route through index.php
RewriteRule ^(.*)$ /index.php [L]
</IfModule>

This is deceptively simple but extremely important. Here is what happens for different requests:

Request URLFile Exists?What Happens
/style.cssYesServed directly (conditions fail)
/images/logo.pngYesServed directly (conditions fail)
/aboutNoRewritten to /index.php
/users/42/profileNoRewritten 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 string
<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 exclusions
<IfModule mod_rewrite.c>
RewriteEngine On

# Skip rewriting for these directories
RewriteRule ^(admin|api|assets)/ - [L]

# Front controller for everything else
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.

note

When using a front controller, make sure your application handles 404 errors internally. Since Apache is routing all non-file requests to index.php, it will always return a 200 status code unless your PHP code explicitly sets a 404 header for unknown routes.

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 routing
<IfModule mod_rewrite.c>
RewriteEngine On

# Route language-prefixed URLs to the appropriate directory or handler
RewriteRule ^(en|fr|de|es)/(.*)$ /content/$1/$2 [L]

# Default to English if no language prefix is present
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /content/en/$1 [L]
</IfModule>
Request URLInternally 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 redirect
<IfModule mod_rewrite.c>
RewriteEngine On

# Only redirect the homepage, not subpages
# Only redirect if no language prefix is already present
RewriteCond %{REQUEST_URI} ^/$ [NC]

# Check if the browser prefers French
RewriteCond %{HTTP:Accept-Language} ^fr [NC]
RewriteRule ^$ /fr/ [R=302,L]

# Check if the browser prefers German
RewriteCond %{REQUEST_URI} ^/$ [NC]
RewriteCond %{HTTP:Accept-Language} ^de [NC]
RewriteRule ^$ /de/ [R=302,L]

# Default to English
RewriteCond %{REQUEST_URI} ^/$ [NC]
RewriteRule ^$ /en/ [R=302,L]
</IfModule>
caution

Use 302 (temporary) redirects for language auto-detection, never 301. A permanent redirect would be cached by the browser, and if a French-speaking user later switches their browser language to English, they would still be sent to the French version. A 302 ensures the detection happens fresh each time.

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

Cookie-based language routing
<IfModule mod_rewrite.c>
RewriteEngine On

# If user selects a language via ?lang=xx, set a cookie and redirect
RewriteCond %{QUERY_STRING} lang=(en|fr|de|es)
RewriteRule ^(.*)$ /$1? [CO=user_lang:%1:.example.com:86400:/,R=302,L]

# If a language cookie exists and user is on the homepage, redirect
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 redirect
<IfModule mod_rewrite.c>
RewriteEngine On

# Redirect /index.php?page=about to /about
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 URLRedirects To
/index.php?page=about/about
/index.php?page=contact/contact
/index.phpNo redirect

Rewriting with Multiple Query Parameters

Multiple query parameters
<IfModule mod_rewrite.c>
RewriteEngine On

# Redirect /products.php?category=shoes&id=42 to /products/shoes/42
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:

# Missing trailing ? in the substitution
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:

# Trailing ? removes the original query string
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.

tip

Remember: an empty ? at the end of a substitution URL is not the same as having no ?. The empty ? explicitly tells Apache to discard the original query string. Without it, the original query string is always preserved.

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 mode
<IfModule mod_rewrite.c>
RewriteEngine On

# Serve maintenance page for everyone
RewriteCond %{REQUEST_URI} !^/maintenance\.html$ [NC]
RewriteCond %{REQUEST_URI} !^/assets/ [NC]
RewriteRule ^(.*)$ /maintenance.html [R=503,L]
</IfModule>

# Set the Retry-After header for search engines
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).
warning

Never use a 301 or 302 redirect for maintenance pages. A 301 would tell search engines your entire site has permanently moved to the maintenance page. A 302 is better but still not ideal. The correct status code is 503 Service Unavailable, which explicitly signals temporary downtime.

Maintenance Mode with IP Whitelist

Allow specific IP addresses (your office, VPN, or home connection) to bypass the maintenance page:

Maintenance mode with IP whitelist
<IfModule mod_rewrite.c>
RewriteEngine On

# Allow specific IPs to bypass maintenance
RewriteCond %{REMOTE_ADDR} !^192\.168\.1\.100$
RewriteCond %{REMOTE_ADDR} !^10\.0\.0\.50$

# Don't redirect the maintenance page or its assets
RewriteCond %{REQUEST_URI} !^/maintenance\.html$ [NC]
RewriteCond %{REQUEST_URI} !^/assets/ [NC]

# Send everyone else to maintenance
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 bypass
<IfModule mod_rewrite.c>
RewriteEngine On

# Set bypass cookie when ?bypass=secretkey is present
RewriteCond %{QUERY_STRING} bypass=secretkey123
RewriteRule ^(.*)$ /$1? [CO=maintenance_bypass:1:.example.com:3600:/,L]

# Skip maintenance for users with the bypass cookie
RewriteCond %{HTTP_COOKIE} maintenance_bypass=1
RewriteRule ^ - [L]

# Don't redirect maintenance page assets
RewriteCond %{REQUEST_URI} !^/maintenance\.html$ [NC]
RewriteCond %{REQUEST_URI} !^/assets/ [NC]

# Everyone else gets maintenance page
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:

# Missing exclusion for the maintenance page itself
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:

# Exclude the maintenance page from the rewrite
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 Sheet
<IfModule mod_rewrite.c>
RewriteEngine On

# --- WWW Canonicalization (www to non-www) ---
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]

# --- Domain Canonicalization ---
RewriteCond %{HTTP_HOST} ^(www\.)?old-domain\.com$ [NC]
RewriteRule ^(.*)$ https://new-domain.com/$1 [R=301,L]

# --- Clean URLs (remove .php) ---
RewriteCond %{REQUEST_URI} \.php$ [NC]
RewriteRule ^(.+)\.php$ /$1 [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^(.+)$ $1.php [L]

# --- Query String Redirect ---
RewriteCond %{QUERY_STRING} ^page=([a-zA-Z0-9_-]+)$
RewriteRule ^index\.php$ /%1? [R=301,L]

# --- Front Controller ---
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.