.htaccess Redirects
Redirects are one of the most common and essential tasks you will handle with .htaccess. Whether you are moving a page to a new URL, restructuring your entire site, or forcing HTTPS, understanding how Apache redirects work is a fundamental skill for any web developer or system administrator.
This guide walks you through the Redirect and RedirectMatch directives, explains the difference between temporary and permanent redirects, covers regex-based matching, directory redirections, trailing slash handling, and the critical HTTP-to-HTTPS redirect pattern. By the end, you will have a solid understanding of how to implement redirects correctly and avoid common pitfalls.
How Redirects Work
When a browser or search engine requests a URL, the server can respond with a special HTTP status code that tells the client, "This resource is no longer here; go look at this other URL instead." This is a redirect.
Apache's mod_alias module provides the Redirect and RedirectMatch directives, which let you define these redirections directly inside your .htaccess file. When Apache processes a request and matches a redirect rule, it sends the appropriate HTTP status code along with the new URL in the Location header. The browser then automatically follows the new URL.
The basic flow looks like this:
- The client requests
/old-page. - Apache matches a redirect rule for
/old-page. - Apache responds with a 3xx status code and a
Location: /new-pageheader. - The client automatically requests
/new-page. - Apache serves the content at
/new-page.
All redirect directives in this guide require the mod_alias module to be enabled. It is enabled by default on most Apache installations. You can wrap your rules in <IfModule mod_alias.c> to prevent errors if the module is not loaded.
The Redirect Directive
The Redirect directive is the simplest way to send a visitor from one URL to another. Its syntax is:
Redirect [status] URL-path URL-to-redirect-to
- status (optional): The HTTP status code or keyword to return. Defaults to
temp(302). - URL-path: The original path to match (must start with
/). - URL-to-redirect-to: The destination URL, which can be a path on the same server or a full external URL.
Here is a basic example:
# Redirect /about to /about-us on the same host
Redirect "/about" "/about-us"
When a user visits https://example.com/about, they are automatically redirected to https://example.com/about-us with a 302 Temporary Redirect status.
You can also redirect to a completely different domain:
# Redirect to a different website
Redirect "/service" "https://newsite.example.com/service"
Temporary Redirect (302)
A 302 Temporary Redirect tells browsers and search engines that the move is not permanent. The original URL should still be indexed by search engines, and browsers will not cache the redirect aggressively.
This is the default behavior when you do not specify a status:
# These two lines are equivalent
Redirect "/one" "/two"
Redirect temp "/one" "/two"
Use a 302 redirect when:
- You are temporarily moving a page during maintenance.
- You are running A/B tests.
- You want search engines to keep indexing the original URL.
Permanent Redirect (301)
A 301 Permanent Redirect tells browsers and search engines that the resource has moved forever. Search engines will transfer ranking signals (link equity) from the old URL to the new one, and browsers will cache this redirect.
# Permanent redirect from /old-blog to /blog
Redirect permanent "/old-blog" "/blog"
You can also use the numeric status code directly:
Redirect 301 "/old-blog" "/blog"
Use a 301 redirect when:
- A page has been permanently moved to a new URL.
- You have restructured your site and old URLs will never return.
- You want to consolidate SEO value on the new URL.
Browsers cache 301 redirects aggressively. If you set up a permanent redirect incorrectly, visitors may continue being redirected even after you remove the rule. Always test with a 302 first before switching to 301 in production.
All Available Status Options
Both Redirect and RedirectMatch support the following status keywords:
| Value | HTTP Code | Description |
|---|---|---|
temp | 302 | Temporary redirect. The resource is temporarily at a different URL. This is the default. |
permanent | 301 | Permanent redirect. The resource has moved permanently. |
seeother | 303 | See Other. The resource has been replaced and the client should use GET to retrieve the new URL. |
gone | 410 | Gone. The resource has been permanently removed. The destination URL should be omitted. |
Example of the gone status:
# Tell clients and search engines this page no longer exists
Redirect gone "/discontinued-product"
When using gone (410), you do not provide a destination URL because the resource no longer exists anywhere.
RedirectMatch with Regex
While Redirect performs simple prefix matching, RedirectMatch gives you the power of regular expressions to match URL patterns. This is extremely useful when you need to redirect multiple URLs that follow a common pattern.
The syntax is:
RedirectMatch [status] regex URL-to-redirect-to
Basic Example
Redirect a specific file regardless of whether it has a trailing slash:
RedirectMatch "^/oldfile\.html/?$" "https://example.com/newfile.php"
Let's break down this regex:
^matches the beginning of the URL path./oldfile\.htmlmatches the literal string/oldfile.html(the dot is escaped)./?matches an optional trailing slash.$matches the end of the URL path.
This will match both /oldfile.html and /oldfile.html/.
Using Capture Groups
One of the most powerful features of RedirectMatch is the ability to capture parts of the matched URL and reuse them in the destination:
# Redirect /blog/2023/my-post to /articles/my-post
RedirectMatch "^/blog/[0-9]{4}/(.+)$" "/articles/$1"
In this example:
[0-9]{4}matches any four-digit year.(.+)is a capture group that matches the rest of the URL (the post slug).$1in the destination refers to whatever was captured by the first group.
So a request to /blog/2023/my-great-post would redirect to /articles/my-great-post.
Multiple Capture Groups
You can use multiple capture groups:
# Redirect /products/category/item to /shop/category/item
RedirectMatch "^/products/([^/]+)/([^/]+)$" "/shop/$1/$2"
$1will contain the category.$2will contain the item.
A request to /products/electronics/laptop redirects to /shop/electronics/laptop.
When writing regex patterns, always anchor them with ^ (start) and $ (end) to avoid unintended partial matches. Without anchors, /oldfile.html.bak could also match a rule intended only for /oldfile.html.
Common Mistake: Forgetting to Escape Dots
A dot (.) in regex matches any character, not just a literal dot. This can lead to unexpected behavior.
Wrong approach:
# This matches /oldpage.html, but also /oldpageXhtml, /oldpage-html, etc.
RedirectMatch "^/oldpage.html$" "/newpage.html"
Correct approach:
# Escape the dot to match only a literal period
RedirectMatch "^/oldpage\.html$" "/newpage.html"
Redirecting Entire Directories
A common scenario is redirecting all URLs under one directory to another directory. There are multiple ways to handle this.
Using Redirect (Prefix Matching)
The Redirect directive already performs prefix matching by default. If the requested URL starts with the specified path, everything after it is appended to the destination:
# Redirect everything under /old-section/ to /new-section/
Redirect permanent "/old-section/" "/new-section/"
With this rule:
| Request URL | Redirects To |
|---|---|
/old-section/ | /new-section/ |
/old-section/page1 | /new-section/page1 |
/old-section/sub/page2 | /new-section/sub/page2 |
This is very convenient and one of the reasons Redirect is often preferred over RedirectMatch for directory-level redirects.
Using RedirectMatch for More Control
If you need more control over how directory redirects work, use RedirectMatch:
# Redirect everything under /old-section/ preserving the path
RedirectMatch permanent "^/old-section/(.*)$" "/new-section/$1"
This produces the same result as the Redirect example above, but gives you the flexibility to add conditions, such as excluding certain subpaths:
# Redirect everything under /old-section/ except /old-section/keep-this
RedirectMatch permanent "^/old-section/(?!keep-this)(.*)$" "/new-section/$1"
Common Mistake: Redirect Loops
Be careful when the source and destination overlap. This creates an infinite redirect loop:
Wrong approach:
# DANGER: This creates a loop!
Redirect "/page" "/page/"
Since Redirect uses prefix matching, a request to /page/ still starts with /page, so it matches again, redirecting to /page//, and so on.
Correct approach:
Use RedirectMatch with an exact pattern:
# Only match /page without a trailing slash
RedirectMatch "^/page$" "/page/"
Trailing Slash Handling
Inconsistent trailing slashes can cause duplicate content issues for SEO and confuse users. You often want to enforce a single canonical form, either always with or always without a trailing slash.
Adding a Trailing Slash
To ensure all directory-like URLs end with a trailing slash:
# Add trailing slash to URLs that don't have one (and are not files)
RedirectMatch permanent "^(/[^\.]+[^/])$" "$1/"
This regex breaks down as:
^(/[^\.]+[^/])$matches any path that does not contain a dot (so it skips files likestyle.css) and does not already end with a slash.$1/appends a slash to the captured path.
| Request URL | Redirects To |
|---|---|
/about | /about/ |
/blog/post | /blog/post/ |
/style.css | No redirect |
/about/ | No redirect |
Removing a Trailing Slash
To remove trailing slashes instead:
# Remove trailing slash (except for the root URL)
RedirectMatch permanent "^(.+)/$" "$1"
| Request URL | Redirects To |
|---|---|
/about/ | /about |
/blog/post/ | /blog/post |
/ | No redirect |
Choose one approach, either always adding or always removing trailing slashes, and apply it consistently across your entire site. Mixing both patterns will create redirect loops and confuse search engines.
Common Mistake: Trailing Slash Loop
Wrong approach:
# Rule 1: Remove trailing slash
RedirectMatch permanent "^(.+)/$" "$1"
# Rule 2: Add trailing slash (somewhere else in the file)
RedirectMatch permanent "^(/[^\.]+[^/])$" "$1/"
These two rules contradict each other and create an infinite loop. A request to /about/ triggers Rule 1 (redirecting to /about), which then triggers Rule 2 (redirecting back to /about/), and so on.
Correct approach: Pick one strategy and remove the conflicting rule.
HTTP to HTTPS Redirect
Forcing all traffic to use HTTPS is one of the most important redirects you can implement. It protects your users' data and is also a ranking factor for search engines.
While this is most commonly done with mod_rewrite, you can achieve it with mod_alias in some configurations. However, the mod_rewrite approach is the standard and most reliable method:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
Let's break down each line:
RewriteEngine Onenables the rewrite engine.RewriteCond %{HTTPS} offchecks if the request is NOT using HTTPS.RewriteRule ^(.*)$matches any URL path.https://%{HTTP_HOST}%{REQUEST_URI}constructs the same URL but withhttps://.[L,R=301]makes this a permanent redirect (R=301) and stops processing further rules (L).
Combining HTTPS with www Canonicalization
It is common to enforce both HTTPS and a canonical domain (with or without www) in a single set of rules:
<IfModule mod_rewrite.c>
RewriteEngine On
# Redirect HTTP to HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Redirect www to non-www
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1%{REQUEST_URI} [L,R=301]
</IfModule>
Or if you prefer to redirect non-www to www:
<IfModule mod_rewrite.c>
RewriteEngine On
# Redirect HTTP to HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Redirect non-www to www
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
Always test HTTPS redirects thoroughly. If your SSL certificate is not properly configured, a redirect to HTTPS will result in a security warning or a connection error for your users. Make sure your certificate covers all domains and subdomains you are redirecting to.
Common Mistake: Redirect Loop with HTTPS Behind a Proxy
If your server is behind a load balancer or reverse proxy (such as Cloudflare or AWS ELB), the connection between the proxy and your server may be HTTP even when the user connects over HTTPS. This means %{HTTPS} will always be off, causing an infinite redirect loop.
Wrong approach (behind a proxy):
# This loops because the proxy connects to Apache over HTTP
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Correct approach (behind a proxy):
Check the X-Forwarded-Proto header instead:
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Quick Reference
Here is a summary of the most common redirect patterns:
<IfModule mod_alias.c>
# Simple redirect (302 temporary by default)
Redirect "/old" "/new"
# Permanent redirect (301)
Redirect permanent "/old-page" "/new-page"
# Redirect to external URL
Redirect 301 "/service" "https://other-site.com/service"
# Regex-based redirect
RedirectMatch permanent "^/blog/([0-9]{4})/(.+)$" "/articles/$2"
# Mark a resource as gone (410)
Redirect gone "/removed-page"
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
# HTTP to HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
Always test your redirect rules using browser developer tools (Network tab) or command-line tools like curl -I https://example.com/old-page to verify the correct status code and destination URL before deploying to production.
Understanding and correctly implementing redirects is a foundational skill that directly impacts user experience and SEO performance. Start with simple Redirect directives for straightforward URL changes, move to RedirectMatch when you need pattern matching, and use mod_rewrite for more complex scenarios like protocol enforcement. Always prefer 301 for permanent changes and 302 for temporary ones, and remember to test thoroughly to avoid redirect loops.