To add a permanent 301 redirect in .htaccess, put a line such as Redirect 301 /old-page /new-page/ in the .htaccess file in your site’s root folder, above the # BEGIN WordPress block, then check the response with curl -I. The table below has copy-paste code for the common cases, and the troubleshooting section covers what to check when a 301 redirect is not working. Every snippet in this guide was tested on Apache 2.4 before publishing.
What is the .htaccess code for a 301 redirect?
Apache gives you two ways to write a 301 redirect in .htaccess. Redirect 301 and RedirectMatch 301 come from mod_alias and match on the URL path. RewriteRule with the [R=301,L] flags comes from mod_rewrite and can also check the hostname, HTTPS and the query string. Swap in your own domain and paths.
| What you want to redirect | .htaccess code |
|---|---|
| One page to a new URL | Redirect 301 /old-page /new-page/ |
| One page to another domain | Redirect 301 /old-page https://newsite.com/new-page/ |
| A folder and everything in it | Redirect 301 /old-folder /new-folder |
| A URL pattern, e.g. dated permalinks to /post-name/ | RedirectMatch 301 ^/(\d{4})/(\d{2})/([^/]+)/?$ https://example.com/$3/ |
| One page, using RewriteRule | RewriteEngine On |
| HTTP to HTTPS | RewriteEngine On |
| Non-www to www | RewriteEngine On |
| www to non-www | RewriteEngine On |
| HTTPS and www in one redirect | RewriteEngine On |
| HTTPS and non-www in one redirect | RewriteEngine On |
| A whole domain to a new domain | RewriteEngine On |
| Add a trailing slash | RewriteEngine On |
| Remove a trailing slash | RewriteEngine On |
| Old .html pages to clean URLs | RewriteEngine On |
| An old URL with a query string (?id=12) | RewriteEngine On |
RewriteEngine On only needs to appear once, before your first RewriteRule. Repeating it does no harm, so each row above works on its own. All of these rules assume the .htaccess file sits in the web root.
Redirect 301: single pages and folders
Redirect is the simplest option and doesn’t need RewriteEngine. The old path must start with a slash. The target can be a full URL or a path; with a path, Apache adds your current scheme and hostname.
Redirect matches by prefix, in whole path segments. Redirect 301 /old-folder /new-folder also sends /old-folder/page/ to /new-folder/page/, but it leaves /old-folder-2/ alone. Any query string on the request is carried over to the target. Two details catch people out:
- A trailing slash on the old path makes it stricter.
Redirect 301 /old-page/ /new-page/does not match a request for/old-page. Write the old path without the slash and both versions redirect. - The old path is case-sensitive, and a path with spaces must be in quotes:
Redirect 301 "/old page.html" /new-page/. Without the quotes Apache returns a 500 error.
RedirectMatch 301: patterns
RedirectMatch works like Redirect but takes a regular expression, matched against the full path including its leading slash. Captured groups go into the target as $1, $2 and so on. The dated-permalink example in the table turns /2019/05/my-post/ into /my-post/, which is what you need after switching WordPress from date-based permalinks to post-name permalinks.
Always anchor the pattern with ^ and $. An unanchored RedirectMatch 301 /old https://example.com/new also matches /blog/old-news/, because the pattern can match anywhere in the path.
RewriteRule [R=301,L]: hosts, HTTPS and query strings
Use RewriteRule when the redirect depends on something other than the path. RewriteCond lines above a rule add conditions, such as the hostname (%{HTTP_HOST}), whether the request came over HTTPS (%{HTTPS}) or the query string (%{QUERY_STRING}). The flags at the end control the result:
R=301sends a permanent redirect. A plain[R]sends a 302, which is a common reason a “301” turns out to be temporary.Lstops processing the remaining rules. Apache’s docs say you will almost always want[R]together with[L].NCmakes the match case-insensitive.QSDdrops the original query string from the target (Apache 2.4.0 and later). Without it, the query string is passed through unchanged.
The host rules in the table put %{REQUEST_URI} in the target, which carries the full path across, and the query string follows automatically because the target has no ? of its own. Combining the HTTPS and www checks into one rule with [OR] saves a hop: http://www.example.com/page goes straight to the final URL.
Moving a whole domain
The whole-domain rule goes in the .htaccess of the old domain and keeps every path, so olddomain.com/about/team/ lands on newdomain.com/about/team/. If the old domain has its own folder, Redirect 301 / https://newdomain.com/ does the same job in one line. Google’s site move guide says to keep the redirects “for as long as possible, generally at least 1 year” (source). A domain move has more steps than the redirect itself, so read the WordPress migration checklist before you switch.
Trailing slashes and file extensions
On WordPress, leave trailing slashes to your permalink setting, which already redirects the other version. Apache’s mod_dir adds the slash for real directories by default, so the trailing-slash rules are for static sites or custom setups. The .html rule skips files that still exist on disk, which helps when you’re moving an old static site into WordPress page by page.
Where do 301 redirects go in a WordPress .htaccess file?
Put your redirect rules at the top of .htaccess, above the # BEGIN WordPress line. WordPress manages everything between # BEGIN WordPress and # END WordPress, and its last rule sends every URL that isn’t a real file to index.php. A RewriteRule placed below that block never sees the old URL.
Here is a complete example. Page-level redirects come first and use a full https:// target, so a visitor on http://www. reaches the new page in one hop. The HTTPS and non-www rule comes next, then WordPress’s own block from the WordPress docs.
# Redirects: keep this section above # BEGIN WordPress
RewriteEngine On
# 1. Page and folder redirects
RewriteRule ^old-page/?$ https://example.com/new-page/ [R=301,L]
RewriteRule ^old-folder(/.*)?$ https://example.com/new-folder$1 [R=301,L]
# 2. Force HTTPS and non-www in one hop
RewriteCond %{HTTPS} !=on [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
If you add redirects after a launch or a redesign, the post-launch checklist shows where they fit with the other launch-day checks.
Why is my 301 redirect not working in .htaccess?
When a 301 redirect in .htaccess does nothing, Apache is usually ignoring the file, the module behind the rule isn’t loaded, the rule sits below WordPress’s block, or the pattern doesn’t match the URL Apache sees. A cached redirect in your browser or CDN can also hide a fix that already works. Go through these checks in order.
1. Apache is ignoring .htaccess (AllowOverride None)
Apache only reads .htaccess when the server config allows it. The default for AllowOverride is None, which means the file is ignored completely. Redirect and RewriteRule both need at least AllowOverride FileInfo. The Apache .htaccess guide has a quick test: type a nonsense word such as TestMe on its own line in the file and reload the site. A 500 error means Apache reads the file (remove the word straight away). No error means it’s ignored, and you need your host to change the setting.
2. The server runs Nginx
Nginx doesn’t read .htaccess files at all. Run curl -I https://example.com/ and look at the Server header; some hosts hide it, so ask support if it’s missing. On Nginx the redirect goes in the server block, for example return 301 https://example.com$request_uri;, as shown in the Nginx guide to converting rewrite rules.
3. mod_rewrite isn’t loaded, or FollowSymLinks is off
If mod_rewrite isn’t loaded, a bare RewriteEngine On causes a 500 error, and the error log shows Invalid command 'RewriteEngine', perhaps misspelled or defined by a module not included in the server configuration. Rules wrapped in <IfModule mod_rewrite.c> fail silently instead. mod_rewrite in .htaccess also needs the FollowSymLinks or SymLinksIfOwnerMatch option; without it, requests return 403 Forbidden. Redirect and RedirectMatch come from mod_alias, so they keep working in both cases.
4. The rule sits below the WordPress block
A RewriteRule placed under # END WordPress never fired for an old URL, because WordPress had already rewritten the request to index.php. Move your rules above # BEGIN WordPress. A plain Redirect 301 line still worked from the bottom of the file, but keeping all redirects in one section at the top makes them easier to find later.
5. Redirect and RewriteRule are mixed
Apache’s “When not to use mod_rewrite” page warns that mixing the two modules makes the execution order depend on context, and the technical docs advise choosing one module for a task. In my test on Apache 2.4.67, a RewriteRule redirect won over a Redirect for the same URL even when the Redirect line came first in the file. WordPress already uses mod_rewrite, so on a WordPress site I write every redirect as a RewriteRule.
6. The pattern doesn’t match the URL Apache sees
- In .htaccess, a RewriteRule pattern is matched against the path without its leading slash.
RewriteRule ^/old-page$ ...never matches; write^old-page$. - Redirect is case-sensitive. For a case-insensitive match, use a RewriteRule with
[NC]. - A trailing slash on the old path in a Redirect line means the version without the slash won’t match.
- Only the path is matched. Domain names belong in a
RewriteCond %{HTTP_HOST}line.
7. Regex characters aren’t escaped
In a regular expression, . matches any character, so ^www.example.com also matches wwwXexample.com. Escape dots as \., and escape ?, +, ( and ) when you mean them literally. Anchor RedirectMatch and RewriteRule patterns with ^ and $ so they don’t match in the middle of other URLs.
8. The old URL has a query string
Redirect, RedirectMatch and the RewriteRule pattern never see the part after ?, so Redirect 301 "/product.php?id=12" /new/ does nothing. Match the query string with RewriteCond %{QUERY_STRING} as in the last row of the table. The (^|&) and (&|$) around id=12 stop it from matching id=123, and QSD stops ?id=12 from being copied onto the new URL.
9. Your browser cached an earlier 301
Under RFC 9110, a 301 can be cached by default, so a browser may keep replaying an old or wrong redirect without asking your server. Test in a private window, with “Disable cache” ticked in DevTools, or with curl, which keeps no cache. While you’re still adjusting a rule, use 302 and switch to 301 once it’s right.
10. A cache plugin, CDN or proxy answers first
Some cache plugins, such as WP Rocket, add their own blocks to .htaccess, and a CDN can serve a stored copy of the old URL before the request reaches Apache. Clear the plugin cache and purge the CDN after changing redirects. If HTTPS ends at a proxy or load balancer, Apache sees every request as plain HTTP, %{HTTPS} is never on, and the HTTPS rule loops. Apache’s redirect recipes suggest checking the X-Forwarded-Proto header instead, and only when you control the proxy:
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !https [NC]
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
If the browser shows “too many redirects”, follow the steps in fixing ERR_TOO_MANY_REDIRECTS in WordPress.
11. The site shows a 500 error after the edit
A 500 right after saving almost always means a syntax error, such as an unquoted path with a space or a missing bracket in the flags. Restore your backup first, then check the error log, which names the line. The guide to fixing a 500 Internal Server Error in WordPress covers the other causes.
How do I test a 301 redirect?
The quickest test is curl -I followed by the old URL, which prints the status code and the Location header the server returns. A working redirect shows 301 Moved Permanently and the new URL. Curl doesn’t cache redirects, so it shows what your server is sending right now.
curl -I https://example.com/old-page
Apache’s response starts like this (HTTP/2 hosts show HTTP/2 301 and a lowercase location:):
HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-page/
To follow the whole chain and count the hops, use curl -sIL https://example.com/old-page. Each block in the output is one hop, and you want one 301 followed by a 200. Here is how to read the status codes:
- 301 with the right
Location: the redirect works. - 302: the rule is missing
301, or uses[R]without=301. - 200 or 404 on the old URL: the rule isn’t matching, or Apache isn’t reading the file.
- 403: mod_rewrite is blocked because FollowSymLinks is off.
- 500: syntax error. Restore your backup.
In a browser, open DevTools, go to the Network tab, tick “Disable cache” and load the old URL. The first request should show 301 and the second should show 200 for the new page.
What is the difference between 301, 302, 307 and 308 redirects?
301 and 308 are permanent redirects; 302 and 307 are temporary. Google treats a permanent redirect as a strong signal that the new URL should appear in search results, and a temporary one as a weak signal. 307 and 308 also forbid the browser from changing a POST request into a GET.
| Code | Type | Request method | .htaccess syntax |
|---|---|---|---|
| 301 | Permanent | Some clients change POST to GET | Redirect 301 or [R=301,L] |
| 302 | Temporary | Some clients change POST to GET | Redirect 302, plain Redirect, or [R=302,L] |
| 307 | Temporary | Method and body kept | Redirect 307 or [R=307,L] |
| 308 | Permanent | Method and body kept | Redirect 308 or [R=308,L] |
Google’s redirects documentation treats 301 and 308 the same way, and 302 and 307 the same way. The method details come from MDN’s redirections guide. For pages that have moved for good, use 301. Use a 302 for a redirect you plan to remove, such as a seasonal landing page. Both Redirect with no status code and RewriteRule with a plain [R] send a 302 by default, so always write the number.
Should you use a plugin or .htaccess for redirects?
On WordPress, a plugin such as Redirection is the safer choice for one-off page redirects. It stores them in the database, gives you a screen to add and edit them, and logs 404s so you can see what still needs redirecting. A typo there can’t take the site down the way a bad .htaccess line can.
Use .htaccess for rules that apply to the whole site and should run before WordPress loads: HTTPS, www or non-www, domain moves, and pattern redirects covering hundreds of old URLs. It’s also the only option on a static site. For a large redesign or platform move, I build the full redirect map before launch and test every old URL against it. If you’d rather hand that part off, my WordPress migration service includes the redirect map and testing.


