Skip to content

.htaccess URL Rewrites

Learn how to use Apache's mod_rewrite for clean URLs, routing, and server-side URL transformations without affecting the client-side address bar.

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/laptop

Apache can internally rewrite this to:

https://example.com/store.php?category=products&item=laptop

The 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=about into /about.
  • Front controller pattern: Routing all requests through a single entry point (like index.php in frameworks such as Laravel or WordPress).
  • API versioning: Mapping /api/v2/users to 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 if mod_rewrite is loaded. This prevents a 500 Internal Server Error if the module is not available.
  • RewriteEngine On: Activates the rewrite engine. Without this line, all RewriteRule and RewriteCond directives are ignored.
  • Options +FollowSymlinks: Required for mod_rewrite to 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/item is tested as products/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} off

Condition 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 -f for "is a file", -d for "is a directory").
RewriteCond %{HTTPS} off

RewriteCond %{HTTP_HOST} =www.example.com

RewriteCond %{REQUEST_FILENAME} !-f

RewriteCond %{REQUEST_FILENAME} !-d

The ! 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:

  • %1 comes from the RewriteCond capture group (.+) and contains the domain without www. (e.g., example.com).
  • $1 comes from the RewriteRule capture 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

  1. Apache receives a request and determines which .htaccess file applies.
  2. RewriteEngine On must be present, or all rules are skipped.
  3. Rules are processed sequentially from top to bottom in the order they appear.
  4. For each RewriteRule, Apache first evaluates all preceding RewriteCond directives (if any). If the conditions pass, the rule's pattern is tested.
  5. If the pattern matches, the substitution is applied.
  6. If the [L] flag is present, Apache stops processing further rules in this pass.
  7. However, in .htaccess context, the rewritten URL is then re-injected into the URL mapping pipeline, and the entire .htaccess ruleset 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:trace6

This 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:

.htaccess URL Rewriting Exampleapache
<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.

Navigation