Guides

.htaccess redirect: 301 rules for pages, folders, HTTPS, www and domain moves

.htaccess redirect rules for Apache: Redirect 301, RedirectMatch and RewriteRule for HTTPS, www, folders, domain moves and query strings, plus curl tests.

Published ~9 min read
On this page

Short answer: to redirect with .htaccess, write Redirect 301 /old-page https://example.com/new-page for a simple move. For anything conditional, such as HTTP to HTTPS or www to non-www, turn on mod_rewrite and pair RewriteCond with a RewriteRule that ends in [R=301,L]. Apache only reads these lines where AllowOverride permits FileInfo.

Apache · .htaccess in the document root
# a single moved page (mod_alias)
Redirect 301 /old-page https://example.com/new-page

# every HTTP request to HTTPS (mod_rewrite)
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

Every directive and flag on this page follows the Apache HTTP Server 2.4 documentation for mod_alias, mod_rewrite, the RewriteRule flags and .htaccess files, read on 21 October 2026. There is no Apache on the machine this was written on, so the rules were not run here: test each one with the curl commands further down before you rely on it.

.htaccess redirect cheat sheet

TaskLines in .htaccess
One pageRedirect 301 /old-page https://example.com/new-page
A folder, keeping the rest of the pathRedirect 301 /old-folder https://example.com/new-folder
Whole site to a new domain, paths keptRedirect 301 / https://new-domain.com/
HTTP to HTTPSRewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
www to non-wwwRewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]
Non-www to wwwRewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^ https://www.example.com%{REQUEST_URI} [R=301,L]
Drop the query stringRewriteRule ^old\.php$ https://example.com/new/ [R=301,L,QSD]
Page removed for good (410)Redirect gone /old-page

The mod_rewrite lines need RewriteEngine On once above them. Each recipe is explained below, with the traps that come with it.

Redirect and RedirectMatch (mod_alias)

Redirect [status] URL-path URL matches a path prefix and appends whatever follows it. So Redirect 301 /service https://example.com/products also sends /service/foo.txt?q=23 to https://example.com/products/foo.txt?q=23. Only complete path segments match: /servicefoo.txt is left alone. The old path must start with a slash, the target is a full URL or a path on the same host, and the query string comes along by itself.

  • No status means 302. Write 301 or permanent for a permanent move; temp is 302, seeother 303, gone 410 (with no target URL).
  • POST bodies are discarded, as the mod_alias docs note. Do not redirect a form or API endpoint this way; point the client at the new address.
  • One page means a prefix too. Redirect 301 /old-page ... also catches /old-page/anything. For exactly one URL, use RedirectMatch with anchors.

RedirectMatch does the same with a regular expression against the URL path, leading slash included, and fills $1 and up from the groups:

Apache · .htaccess
# exactly one URL, with or without a trailing slash
RedirectMatch 301 ^/old-page/?$ https://example.com/new-page

# /blog/2024/some-post  ->  /articles/some-post
RedirectMatch 301 ^/blog/[0-9]{4}/(.+)$ https://example.com/articles/$1

Anchor the pattern with ^ and $: an unanchored RedirectMatch 301 /old https://... also fires for /shop/old-stock.

mod_rewrite: RewriteEngine, RewriteCond and RewriteRule

mod_rewrite is for redirects that depend on something other than the path: the hostname, HTTPS, the query string, whether a file exists. A rule set looks like this:

Apache · .htaccess
RewriteEngine On
RewriteCond TestString CondPattern [flags]     # zero or more conditions, ANDed
RewriteRule Pattern Substitution [flags]       # applies only if all conditions match

Four rules from the documentation explain most broken .htaccess redirects:

  • In .htaccess the pattern never starts with a slash. Apache strips the directory prefix before matching, so ^/old-page$ never matches there; write ^old-page$. In a VirtualHost the slash is part of the path.
  • The query string is not part of the pattern. Match it with RewriteCond %{QUERY_STRING}, and the hostname with %{HTTP_HOST}.
  • Conditions are ANDed. Add [OR] to a condition to combine it with the next one instead, and [NC] to ignore case.
  • $1 comes from the RewriteRule, %1 from the last matched RewriteCond. The www recipe below depends on that difference.
FlagEffect
R=301External redirect with that status; a bare R sends 302
LStop processing this pass of the rule set; use it with every R
ENDStop all further rewriting, also on the next pass in .htaccess
NCCase-insensitive match
QSDDrop the incoming query string (Apache 2.4.0 and later)
QSAAppend the incoming query string to one you set in the target
NEDo not percent-encode the target, so a # anchor stays a #

The docs explain why R wants L: on its own it rewrites the URL and hands it to the next rule, which leads to odd follow-up rewrites. mod_rewrite in .htaccess also needs Options FollowSymLinks or SymLinksIfOwnerMatch in effect, which Debian’s and Ubuntu’s default Apache config already sets for /var/www.

Recipes

HTTP to HTTPS

Apache · .htaccess
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

^ matches every path, %{REQUEST_URI} carries the path across, and the query string is appended on its own. The site needs a working certificate first, or visitors land on a browser warning. If the server sits behind Cloudflare, a load balancer or an nginx that ends TLS, %{HTTPS} is always off and this rule loops; the section on proxies and CDNs below has the fix.

www to non-www, and back

Apache · .htaccess · www.example.com to example.com
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
Apache · .htaccess · example.com to www.example.com
RewriteEngine On
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^ https://www.example.com%{REQUEST_URI} [R=301,L]

The first rule reuses whatever followed www. through %1, so it works for any domain on the account. The second names the bare domain on purpose: the popular variant RewriteCond %{HTTP_HOST} !^www\. also catches subdomains and sends shop.example.com to www.shop.example.com. To do HTTPS and the hostname in one hop instead of two, combine the conditions:

Apache · .htaccess · one hop to https://example.com
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]

Why one hop matters for search engines, and how chains build up, is covered in 301 Moved Permanently.

A whole folder

Apache · .htaccess
# keep the rest of the path: /old-folder/a/b -> /new-folder/a/b
Redirect 301 /old-folder https://example.com/new-folder

# the same with mod_rewrite, if the file already uses it
RewriteRule ^old-folder(/.*)?$ https://example.com/new-folder$1 [R=301,L]

Sending every page of a folder to one single page is possible (RedirectMatch 301 ^/old-folder(/.*)?$ https://example.com/new-page), but search engines may treat mass redirects to an unrelated page as soft 404s. Map old pages to their real successors where they exist.

A domain move that keeps every path

Apache · .htaccess on the old domain
Redirect 301 / https://new-domain.com/

Because Redirect appends the rest of the path and the query string, old-domain.com/blog/post?x=1 lands on new-domain.com/blog/post?x=1. When both domains point at the same document root, limit the rule to the old name:

Apache · .htaccess shared by both domains
RewriteEngine On
RewriteCond %{HTTP_HOST} ^(www\.)?old-domain\.com$ [NC]
RewriteRule ^ https://new-domain.com%{REQUEST_URI} [R=301,L]

Keep a valid certificate on the old domain for as long as the redirect runs: an HTTPS visitor meets the certificate before Apache can send the 301.

Trailing slash

Apache · .htaccess
RewriteEngine On

# remove it: /page/ -> /page (real directories excluded)
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)/$ https://example.com/$1 [R=301,L]

# or add it: /page -> /page/ (files and paths with an extension excluded)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !(\.[a-zA-Z0-9]{1,5}|/)$
RewriteRule ^ https://example.com%{REQUEST_URI}/ [R=301,L]

Use one of the two blocks, never both. Real directories get their slash from mod_dir’s DirectorySlash anyway, which redirects /docs to /docs/ with a 301. On WordPress, leave the slash to the permalink setting: WordPress redirects every URL to its own canonical form, and a rule that disagrees with it loops.

Query strings

Apache · .htaccess
RewriteEngine On

# drop the query string entirely
RewriteRule ^old\.php$ https://example.com/new/ [R=301,L,QSD]

# /product.php?id=42 -> /products/42 (and nothing for other parameters)
RewriteCond %{QUERY_STRING} (?:^|&)id=([0-9]+)(?:&|$)
RewriteRule ^product\.php$ https://example.com/products/%1 [R=301,L,QSD]

# a target with an anchor: NE keeps the # from becoming %23
RewriteRule ^faq-shipping$ https://example.com/faq#shipping [R=301,L,NE]

Without QSD the old query string is copied onto the target, which is how /products/42?id=42 happens. Before QSD arrived in Apache 2.4.0, the same was done with a lone ? at the end of the target, which still works and still shows up in older guides.

301 vs 302 vs 307 vs 308 in .htaccess

CodeMeaningmod_aliasmod_rewrite
301Moved permanently; POST may become GETRedirect 301[R=301,L]
302Temporary; the default of both modulesRedirect[R,L]
307Temporary, method and body keptRedirect 307[R=307,L]
308Permanent, method and body keptRedirect 308[R=308,L]
410Gone, no targetRedirect gone[G]

Browsers cache 301 and 308 answers, often for a long time, so a wrong permanent rule keeps misdirecting people after you fix it. Write new rules with 302 ([R=302,L]), check them, then switch to 301. The full comparison, including what each code does to a POST, is in the 301 guide’s status code table.

Why your .htaccess redirect does nothing

Apache’s default for AllowOverride is None, and Debian and Ubuntu ship AllowOverride None for /var/www. With that, Apache never opens .htaccess and your redirect silently does nothing. Redirect, RedirectMatch and all rewrite directives need the FileInfo category:

/etc/apache2/sites-available/example.com.conf
<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/example.com

    <Directory /var/www/example.com>
        AllowOverride FileInfo
        Options FollowSymLinks
        Require all granted
    </Directory>

    # ... SSLEngine and certificate lines ...
</VirtualHost>
Debian · Ubuntu
sudo a2enmod rewrite
sudo apachectl configtest
sudo systemctl restart apache2

AllowOverride All works too but hands .htaccess more power than redirects need. When the file is read but a directive is not allowed or its module is missing, you get a 500 instead, with not allowed here or Invalid command 'RewriteEngine' in the error log; the 500 error guide lists those messages. If you control the server, the Apache docs recommend putting the rules into the VirtualHost instead: .htaccess is read on every request, while the main config is read once. Remember the leading slash in the pattern when you move a rule there.

Test a redirect with curl -I

Linux · macOS
curl -sI http://example.com/old-page | grep -iE '^(HTTP|location)'
curl -sIL http://www.example.com/old-page | grep -iE '^(HTTP|location)'     # follow the chain
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'http://example.com/product.php?id=42'
Representative output · a correct permanent redirect
HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-page

Check three things: the status code, that Location is the final address (not a hop in between), and that a followed chain ends in 200 after one redirect. Test with a query string and with and without www too. On Windows, type curl.exe. More options are in curl command examples.

Redirect loops behind a proxy or CDN

When HTTPS ends before Apache (Cloudflare in Flexible mode, a load balancer, an nginx in front), every request reaches Apache over plain HTTP. %{HTTPS} is off each time, the HTTPS rule redirects forever, and the browser stops with ERR_TOO_MANY_REDIRECTS. Let the rule accept the scheme the proxy reports:

Apache · .htaccess behind a TLS-terminating proxy
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

A loop inside the server is different: Request exceeded the limit of 10 internal redirects in the error log means a rewrite (not a redirect) keeps matching its own result. In .htaccess the rule set runs again after a rewrite, so add a condition that stops the second pass, or use END instead of L.

Order, WordPress and mixing modules

  • Redirects go at the top, above the # BEGIN WordPress block, which WordPress rewrites itself and which sends every unknown path to index.php before a rule further down could match.
  • Specific rules before general ones, because the first matching rule with L ends the pass.
  • Do not mix Redirect and RewriteRule for the same URLs. The two modules do not run as one list in file order, so the order in the file does not decide which of them wins. Once a site uses mod_rewrite, keep its redirects there.
  • Escape dots in patterns: ^old\.php$, because a bare . matches any character.

nginx has no .htaccess

nginx reads no per-directory files, so .htaccess rules do nothing there and have to move into the server block. The usual equivalents:

nginx · inside the server blocks
# Redirect 301 /old-page https://example.com/new-page
location = /old-page {
    return 301 https://example.com/new-page;
}

# Redirect 301 /old-folder https://example.com/new-folder
location /old-folder {
    rewrite ^/old-folder(/.*)?$ https://example.com/new-folder$1 permanent;
}

# HTTP to HTTPS and www to non-www in one hop
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

$request_uri includes the query string; leave it out of the target to drop it. Then run sudo nginx -t && sudo systemctl reload nginx, as in restarting and reloading nginx. More nginx patterns are in the nginx part of the 301 guide. On VMHeaven’s Plesk hosting, forcing HTTPS is a checkbox in the panel rather than a rule you write, and on a Linux VPS you choose Apache or nginx yourself and can put the rules straight into the server config.

Frequently asked

How do I redirect a page with .htaccess?

Add Redirect 301 /old-page https://example.com/new-page to the .htaccess file in the document root. Redirect matches a path prefix, so it also catches /old-page/anything; for exactly one URL use RedirectMatch 301 ^/old-page/?$ https://example.com/new-page. Without a status code Apache sends a temporary 302.

How do I redirect HTTP to HTTPS in .htaccess?

Use mod_rewrite: RewriteEngine On, then RewriteCond %{HTTPS} off, then RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]. Behind Cloudflare in Flexible mode, a load balancer or an nginx that ends TLS, %{HTTPS} is always off and the rule loops: add RewriteCond %{HTTP:X-Forwarded-Proto} !https, or have the proxy talk HTTPS to the server.

Why does my .htaccess redirect not work?

Most often Apache ignores the file because AllowOverride is None, which is Apache's default and what Debian and Ubuntu set for /var/www; allow FileInfo in the virtual host. RewriteRule also needs mod_rewrite enabled (a2enmod rewrite), and in .htaccess its pattern must not start with a slash. A cached 301 in the browser can hide a fixed rule, so test with curl -I.

Does nginx support .htaccess files?

No. nginx reads no per-directory configuration files, so redirects go into the server block, for example location = /old-page { return 301 https://example.com/new-page; }. Test the config with nginx -t, then reload nginx.

Related articles

All guides