URL rewriting is one of the most powerful features Apache offers through its mod_rewrite module. It allows you to transform how URLs look and behave without changing your underlying file structure. While redirects tell the browser to go somewhere else, rewrites silently map one URL to another on the server side.
This guide covers everything you need to understand URL rewriting: what it is, how it differs from redirecting, the anatomy of rewrite rules and conditions, how to use flags and backreferences, and the order in which Apache processes your rules. By the end, you will be comfortable reading, writing, and debugging rewrite rules in .htaccess.
What is .URL Rewriting?
URL rewriting is the process of internally translating a requested URL into a different URL on the server, without the client ever knowing. The browser still sees the original URL in the address bar, but Apache serves content from a different location or script behind the scenes.
For example, when a user visits:
https://example.com/products/laptopApache can internally rewrite this to:
https://example.com/store.php?category=products&item=laptopThe user sees the clean, readable URL. The server processes the request through store.php with query parameters. Everyone wins.
Common use cases for URL rewriting include:
- Clean URLs: Turning
/index.php?page=aboutinto/about. - Front controller pattern: Routing all requests through a single entry point (like
index.phpin frameworks such as Laravel or WordPress). - API versioning: Mapping
/api/v2/usersto an internal script. - Content negotiation: Serving different resources based on request characteristics.
Rewrite vs Redirect
Understanding the difference between rewriting and redirecting is essential before diving into the syntax. These two concepts are often confused, but they serve very different purposes.
| Aspect | Rewrite | Redirect |
|---|---|---|
| Visibility | Internal, the browser URL does not change | External, the browser navigates to a new URL |
| HTTP roundtrips | One (the server handles it internally) | Two (the server tells the browser to re-request) |
| Browser awareness | The browser has no idea a rewrite happened | The browser sees the new URL |
| SEO impact | No impact, content appears to live at the original URL | Transfers or splits SEO value depending on status code |
| Use case | Clean URLs, routing to scripts | Moved pages, protocol changes, domain changes |
Here is a practical comparison:
RewriteRule ^about$ /pages/about.html [L]
RewriteRule ^old-about$ /new-about [R=301,L]Enabling the Rewrite Engine
Before any rewrite rule can work, you must explicitly enable the rewrite engine. This is a required step that is easy to forget and is the number one reason rewrite rules silently fail.
<IfModule mod_rewrite.c>
RewriteEngine On
Options +FollowSymlinks
</IfModule>Let's break down each part:
<IfModule mod_rewrite.c>: Wraps the block so it only executes ifmod_rewriteis loaded. This prevents a 500 Internal Server Error if the module is not available.RewriteEngine On: Activates the rewrite engine. Without this line, allRewriteRuleandRewriteConddirectives are ignored.Options +FollowSymlinks: Required formod_rewriteto function correctly. It allows Apache to follow symbolic links in the directory.
Hosting-Specific Considerations
Depending on your hosting environment, you may need additional configuration:
<IfModule mod_rewrite.c>
RewriteEngine On
Options +FollowSymlinks
</IfModule>RewriteRule Anatomy
The RewriteRule directive is where the actual URL transformation happens. Its syntax has three parts:
RewriteRule Pattern Substitution [Flags]Let's examine each component in detail.
Pattern
The pattern is a regular expression that is tested against the URL path of the incoming request. In .htaccess context, the path is relative to the directory containing the .htaccess file, and it does not include a leading slash.
RewriteRule ^about$ /pages/about.html [L]Important details about the pattern:
- It is a Perl-compatible regular expression (PCRE).
- In
.htaccess, the tested path has no leading slash. The URL/products/itemis tested asproducts/item. - The pattern
^$matches the root URL (/). - A pattern of
^(.*)$matches everything.
RewriteRule ^$ /home.php [L]
RewriteRule ^(.*)$ /index.php [L]
RewriteRule ^blog/(.*)$ /blog.php?slug=$1 [L]Substitution
The substitution is the string that replaces the original URL when the pattern matches. It can be:
- A file path relative to the document root.
- An absolute URL (which automatically triggers a redirect).
- A single hyphen (
-) meaning "do not substitute" (useful when you only want to set flags or environment variables).
RewriteRule ^user/([a-z]+)$ /profile.php?username=$1 [L]
RewriteRule ^old-site/(.*)$ https://new-site.com/$1 [R=301,L]
RewriteRule ^secure/ - [E=REQUIRE_AUTH:1]Flags
Flags are enclosed in square brackets [...] and modify the behavior of the rewrite rule. Multiple flags are separated by commas. These are the most important ones you will use regularly.
L (Last)
The L flag tells Apache to stop processing further rewrite rules if this rule matches. Without it, Apache continues evaluating subsequent rules, which can lead to unexpected behavior.
RewriteRule ^about$ /pages/about.html [L]
RewriteRule ^(.*)$ /index.php [L]Without [L] on the first rule, a request to /about would first be rewritten to /pages/about.html, and then the second rule would rewrite it again to /index.php. The L flag prevents this cascade.
R (Redirect)
The R flag forces an external redirect instead of an internal rewrite. You can optionally specify the HTTP status code.
RewriteRule ^old$ /new [R]
RewriteRule ^old$ /new [R=301]
RewriteRule ^old$ /new [R=301,L]When would you use R instead of a plain Redirect directive? When you need the power of regex matching or conditions (RewriteCond) to control when the redirect happens.
NC (NoCase)
The NC flag makes the pattern match case-insensitive.
RewriteRule ^about$ /pages/about.html [NC,L]Without NC, the pattern ^about$ only matches the exact lowercase string about.
QSA (Query String Append)
The QSA flag appends the original query string to the substitution URL instead of discarding it.
Without QSA:
RewriteRule ^search/(.+)$ /search.php?query=$1 [L]A request to /search/laptops?page=2 becomes /search.php?query=laptops. The ?page=2 is lost.
With QSA:
RewriteRule ^search/(.+)$ /search.php?query=$1 [QSA,L]A request to /search/laptops?page=2 becomes /search.php?query=laptops&page=2. The original query string is preserved and appended.
Other Common Flags
| Flag | Name | Description |
|---|---|---|
END |
End | Fully stops rewriting (unlike L, it prevents re-processing in .htaccess). Apache 2.3.9+. |
F |
Forbidden | Returns a 403 Forbidden response immediately. |
G |
Gone | Returns a 410 Gone response immediately. |
NE |
No Escape | Prevents special characters in the substitution from being hex-encoded. |
P |
Proxy | Forces the substitution to be handled as a proxy request (requires mod_proxy). |
S=N |
Skip | Skips the next N rules if this rule matches. |
E=VAR:VAL |
Environment Variable | Sets an environment variable VAR to the value VAL. |
CO |
Cookie | Sets a cookie in the response. |
Example combining multiple flags:
RewriteRule (^\.|/\.) - [F,L]
RewriteRule ^api/(.*)$ http://backend-server:8080/$1 [P,L]RewriteCond Anatomy
A RewriteCond directive defines a condition that must be true for the immediately following RewriteRule to be applied. Without conditions, a rewrite rule runs on every single request that matches its pattern. Conditions give you precise control.
The syntax is:
RewriteCond TestString ConditionPattern [Flags]Test String
The test string is the value you want to test. It typically contains server variables enclosed in %{VARIABLE_NAME} syntax. It can also include backreferences and literal text.
RewriteCond %{HTTPS} offCondition Pattern
The condition pattern is what the test string is compared against. It can be:
- A regular expression (default behavior).
- A string comparison using lexicographic operators like
<,>,=. - A special test prefixed with
-(such as-ffor "is a file",-dfor "is a directory").
RewriteCond %{HTTPS} off
RewriteCond %{HTTP_HOST} =www.example.com
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-dThe ! prefix negates the condition. So !-f means "is NOT an existing file."
Server Variables
Server variables give you access to virtually every aspect of the incoming request. Here are the most commonly used ones:
REQUEST_URI
The full path component of the requested URL, including the leading slash.
RewriteCond %{REQUEST_URI} ^/api/
RewriteRule ^(.*)$ /api-handler.php [L]HTTP_HOST
The hostname from the Host: header of the request (e.g., www.example.com).
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1%{REQUEST_URI} [R=301,L]HTTPS
Contains on if the connection uses SSL/TLS, or off otherwise.
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]REQUEST_FILENAME
The full local filesystem path to the file or directory that matches the request. This is extremely useful for the front controller pattern.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]This is the classic rewrite pattern used by WordPress, Laravel, Symfony, and virtually every PHP framework.
QUERY_STRING
The query string portion of the URL (everything after ?), without the leading ?.
RewriteCond %{QUERY_STRING} ^id=([0-9]+)$
RewriteRule ^product\.php$ /products/%1? [R=301,L]The trailing ? in the substitution removes the original query string. Without it, the query string would be carried over.
Combining Multiple Conditions
When you stack multiple RewriteCond directives before a RewriteRule, they are combined with an implicit AND logic. All conditions must be true for the rule to fire.
RewriteCond %{HTTPS} off
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]This rule only fires if the request is not HTTPS AND the host starts with www. If only one condition is true, the rule is skipped.
To use OR logic instead, add the [OR] flag to the conditions:
RewriteCond %{HTTP_HOST} ^old-domain\.com$ [NC,OR]
RewriteCond %{HTTP_HOST} ^legacy-domain\.com$ [NC]
RewriteRule ^(.*)$ https://new-domain.com/$1 [R=301,L]This redirects traffic from either old-domain.com or legacy-domain.com to new-domain.com.
You can mix AND and OR logic:
RewriteCond %{HTTP_HOST} ^old-domain\.com$ [NC,OR]
RewriteCond %{HTTP_HOST} ^legacy-domain\.com$ [NC]
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://new-domain.com/$1 [R=301,L]Backreferences
Backreferences let you capture parts of matched strings and reuse them in the substitution or in subsequent conditions. There are two types, and confusing them is a very common source of bugs.
Rule Backreferences ($1, $2, ...)
When the pattern of a RewriteRule contains parenthesized groups, the captured values are available as $1, $2, $3, and so on, in the substitution string.
RewriteRule ^blog/([0-9]{4})/([a-z-]+)$ /blog.php?year=$1&slug=$2 [L]A request to /blog/2024/my-post is internally rewritten to /blog.php?year=2024&slug=my-post.
Condition Backreferences (%1, %2, ...)
When a RewriteCond pattern contains parenthesized groups, the captured values are available as %1, %2, %3, and so on. These can be used in the substitution of the following RewriteRule or in subsequent RewriteCond test strings.
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]Here:
%1comes from theRewriteCondcapture group(.+)and contains the domain withoutwww.(e.g.,example.com).$1comes from theRewriteRulecapture group(.*)and contains the URL path.
Common Mistake: Mixing Up $ and %
Wrong approach:
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://$1/$1 [R=301,L]If someone visits www.example.com/about, you might expect https://example.com/about, but instead you get https://about/about because $1 refers to the RewriteRule capture (which is about), not the RewriteCond capture.
Correct approach:
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]Quick reference:
| Syntax | Source | Example |
|---|---|---|
$1 |
RewriteRule pattern |
Captured from the URL path |
%1 |
RewriteCond pattern |
Captured from a condition |
Rewrite Order of Processing
Understanding the order in which Apache processes rewrite rules is critical for debugging and writing correct configurations. Here is how it works in .htaccess:
Step-by-Step Processing
- Apache receives a request and determines which
.htaccessfile applies. RewriteEngine Onmust be present, or all rules are skipped.- Rules are processed sequentially from top to bottom in the order they appear.
- For each
RewriteRule, Apache first evaluates all precedingRewriteConddirectives (if any). If the conditions pass, the rule's pattern is tested. - If the pattern matches, the substitution is applied.
- If the
[L]flag is present, Apache stops processing further rules in this pass. - However, in
.htaccesscontext, the rewritten URL is then re-injected into the URL mapping pipeline, and the entire.htaccessruleset runs again from the top with the new URL. This loop continues until no rule matches or the URL stabilizes.
Practical Implications
This re-processing behavior is the most common source of confusion and infinite loops in .htaccess rewrite rules. Consider this example:
Wrong approach:
RewriteRule ^(.*)$ /index.php [L]On the first pass, /about is rewritten to /index.php. Then the rules run again, and /index.php matches ^(.*)$, rewriting to /index.php again. Apache detects the loop after about 10 iterations and returns a 500 Internal Server Error.
Correct approach:
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]Now, on the second pass, /index.php is an existing file, so !-f fails and the rule is skipped. The loop is broken.
Rule Ordering Best Practices
The order of your rules matters. Follow these guidelines:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
RewriteRule ^old-page$ /new-page [R=301,L]
RewriteRule ^legacy/(.*)$ /modern/$1 [R=301,L]
RewriteRule (^\.|/\.) - [F,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]
</IfModule>Debugging Tip
If your rules are not behaving as expected, you can enable the rewrite log (in the server config, not .htaccess) to see exactly what Apache is doing:
LogLevel alert rewrite:trace6This generates detailed log entries showing which rules and conditions are being evaluated, what they match, and what the substitution result is. Set the trace level from trace1 (minimal) to trace8 (extremely verbose).
Putting It All Together
Here is a complete, well-structured .htaccess file that demonstrates the concepts covered in this guide:
<IfModule mod_rewrite.c>
RewriteEngine On
Options +FollowSymlinks
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1%{REQUEST_URI} [R=301,L]
RewriteRule ^old-about$ /about [R=301,L]
RewriteRule ^blog/([0-9]{4})/([0-9]{2})/(.+)$ /articles/$3 [R=301,L]
RewriteRule ^user/([a-zA-Z0-9_-]+)$ /profile.php?name=$1 [QSA,L]
RewriteRule (^\.|/\.) - [F,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]
</IfModule>This file follows the recommended order: protocol enforcement first, then domain canonicalization, then specific redirects, then clean URL rewrites, then security rules, and finally the catch-all front controller. Each rule uses the [L] flag to prevent unnecessary processing of subsequent rules when a match is found.
Mastering mod_rewrite takes practice, but once you understand the anatomy of rules and conditions, the role of flags, and the processing order, you will be able to handle virtually any URL manipulation scenario with confidence.