To do a 301 redirect, add a rule where your site is served that answers the old URL with status 301 and the new address: a RewriteRule in .htaccess on Apache, a return 301 on nginx, a Redirect Rule on Cloudflare, a plugin entry in WordPress, or a line in _redirects or vercel.json on Netlify and Vercel. This guide gives the exact rule for each, for one page and for a list of hundreds, and shows how to test it and what to check when it does nothing. For what a 301 is and how it affects rankings, read the 301 redirects guide first; allow 15 minutes for a single redirect and an hour for a list.
Quick answer
- Apache or LiteSpeed:
RewriteRule ^old-page/?$ /new-page/ [R=301,L]in.htaccess, above the WordPress block. - nginx:
location = /old-page { return 301 /new-page/; }inside theserverblock, thennginx -tand a reload. - Cloudflare: a Redirect Rule for patterns and a few pages, Bulk Redirects for long lists of exact URLs.
- WordPress: a redirect plugin for editors, or server rules above
# BEGIN WordPressfor speed. - Netlify:
/old-page /new-page/ 301in_redirects. Vercel: aredirectsentry invercel.json. - Many URLs: list old and new in two columns and generate the rules with the redirect rule generator.
- Test with the redirect checker or
curl -sI: one 301, then a 200. Say 301 explicitly: several tools send a 302 by default, which 301 vs 302 explains.
What the server sends
Whatever the platform, the result is the same response. The old URL returns no page, only a status and a Location header:
GET /old-page HTTP/1.1
Host: www.example.com
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/new-page/Two rules apply everywhere: point at the final URL (right scheme, host and trailing slash, so it lands in one hop), and point at the closest equivalent page, never everything at the home page. Where there is no equivalent, 404 vs 410 vs redirect helps you decide.
How to set up a 301 redirect in .htaccess (Apache)
.htaccess sits in the site root on Apache and LiteSpeed. Rules run top to bottom, so put redirects above any CMS block (in WordPress, above # BEGIN WordPress).
One page, with mod_rewrite:
RewriteEngine On
# /old-page and /old-page/ → /new-page/
RewriteRule ^old-page/?$ /new-page/ [R=301,L]
# A page moved to another folder
RewriteRule ^blog/2019/summer-sale/?$ /offers/ [R=301,L]In .htaccess the pattern has no leading slash (^old-page, not ^/old-page), and without R=301 mod_rewrite sends a 302.
A whole folder, keeping the rest of the path:
RewriteRule ^shop/(.*)$ /store/$1 [R=301,L]An old URL with a query string. The pattern never sees the query, so match it with a condition, and add QSD (Apache 2.4) to drop it from the target:
RewriteCond %{QUERY_STRING} ^page=contact$
RewriteRule ^index\.php$ /contact/ [R=301,L,QSD]With mod_alias: Redirect 301 /old-page https://www.example.com/new-page/ is shorter, but it is a prefix match: it also sends /old-page/anything to /new-page/anything. For exact matches use RedirectMatch 301 ^/old-page/?$ https://www.example.com/new-page/. If the file already uses RewriteRule (WordPress does), stick to it so the order stays readable.
How to set up a 301 redirect in nginx
nginx has no .htaccess; rules go in the site's server block (for example /etc/nginx/sites-available/example.com), followed by nginx -t and systemctl reload nginx.
One page:
location = /old-page {
return 301 /new-page/;
}location = is an exact match, so /old-page/ needs its own block or a regex (location ~ ^/old-page/?$).
A folder:
location ^~ /shop/ {
rewrite ^/shop/(.*)$ /store/$1 permanent;
}Many pages, with a map. A map is a lookup table that stays fast with thousands of rows. It goes in the http context, for example /etc/nginx/conf.d/redirects.conf:
map $uri $redirect_to {
default "";
/old-page /new-page/;
/blog/2019/summer-sale /offers/;
/about-us.html /about/;
}Then, in the site's server block:
if ($redirect_to) {
return 301 $redirect_to$is_args$args;
}$uri ignores the query string and $is_args$args passes it on; to match old URLs that include a query string, key the map on $request_uri instead, as the redirect generator does. If nginx refuses to start with "could not build map_hash", add map_hash_bucket_size 128; to the http block. For http → https and www in one rule, see trailing slashes, www and https.
How to set up Cloudflare redirect rules and Bulk Redirects
Cloudflare answers the redirect at the edge, before your server is involved. The host name needs a proxied DNS record (orange cloud), or the rule never sees the request.
Redirect Rules (Cloudflare's docs call them Single Redirects), for patterns and a handful of pages: go to Rules → Overview → Create rule → Redirect Rule.
- Static target: match
http.request.uri.path eq "/old-page", URLhttps://www.example.com/new-page/, status 301, "Preserve query string" on. - Pattern: use the wildcard fields, request URL
https://www.example.com/shop/*and targethttps://www.example.com/store/${1}, which moves/shop/mugsto/store/mugs. Older rules written as expressions withconcat()work too.
The number of rules and the regex support depend on your plan.
Bulk Redirects, for hundreds or thousands of exact URLs, are set up at account level: create a Bulk Redirect List, import a CSV, then create a Bulk Redirect Rule that enables the list. Each line is source, target and status:
www.example.com/old-page,https://www.example.com/new-page/,301
www.example.com/blog/2019/summer-sale,https://www.example.com/offers/,301A source without http:// or https:// matches both. Cloudflare's importer expects no header row, and source URLs cannot contain a query string. The redirect generator writes this format.
How to set up 301 redirects in WordPress
WordPress already redirects one case itself: when you change the slug of a published post, it remembers the old slug and 301-redirects it. Pages are not covered, and nothing else is. Choose between a plugin and the server:
- Redirection (free): Tools → Redirection → Add new, source
/old-page/, target/new-page/, type "301 Moved Permanently". It imports CSV lists and logs 404s. - Rank Math (free): enable the Redirections module, then Rank Math → Redirections.
- Yoast SEO Premium: a redirect manager that offers a redirect when you delete or move a page.
Use one redirect plugin, not two. A plugin rule only fires after WordPress has loaded PHP and the database, so for large lists server rules are faster: on Apache or LiteSpeed, put them above # BEGIN WordPress, which WordPress never rewrites. The WordPress redirects guide compares the plugins and shows which layer should own which redirect.
How to set up Netlify redirects and Vercel redirects
Netlify: a file called _redirects in the publish folder, one rule per line. The status defaults to 301 when you leave it out; ! forces the rule even if a file still exists at the old path:
/old-page /new-page/ 301
/blog/2019/* /archive/:splat 301
https://old-domain.com/* https://www.new-domain.com/:splat 301!The domain rule only works when old-domain.com is added to the same Netlify site as a domain alias. The same rules can live in netlify.toml under [[redirects]].
Vercel: a redirects array in vercel.json. "permanent": true sends a 308, which Google treats like a 301; use "statusCode": 301 if you want a 301 specifically (not both in one rule):
{
"redirects": [
{ "source": "/old-page", "destination": "/new-page/", "statusCode": 301 },
{ "source": "/blog/:slug", "destination": "/articles/:slug", "permanent": true }
]
}vercel.json holds up to 2,048 redirects. Beyond that, Vercel's bulk redirects read CSV or JSON files named by bulkRedirectsPath (on Pro and Enterprise plans). Next.js sites can use redirects() in next.config.js with the same fields.
How to redirect many URLs at once
Build a redirect map: old URL in one column, new URL in the next, one row per page that changed. Take the old URLs from your sitemap, analytics landing pages, Search Console and backlinks, so nothing that gets traffic or links is missed; redirect mapping covers collecting and matching them.
Paste the two columns and choose your server: Apache, nginx, Cloudflare Bulk Redirects, Netlify, Vercel or Caddy. It runs in your browser for up to 10,000 rows, flags duplicates, conflicts and redirects to themselves, removes loops and points chains (A → B → C) straight at the final page before writing a rule. Every rule is an exact match, so /shop redirects /shop and nothing below it.
To move a whole domain, redirect every path to the same path on the new domain in one rule, keeping the query string, and keep the old domain registered with a valid certificate. How to redirect a domain to another domain has the rules for each server and registrar.
Why is my .htaccess 301 redirect not working?
Work down this list; one of these is almost always the reason:
- The server is not Apache. nginx ignores
.htaccesscompletely. LiteSpeed reads it; nginx-based hosts need the rule in their panel or config. .htaccessis not read. WithAllowOverride None, Apache ignores the file. Type a line of nonsense at the top: if the site does not answer with a 500 error, the file is not read.- mod_rewrite is off. Enable it (
a2enmod rewriteand a restart) or ask the host. - The rule is below the CMS block. WordPress's catch-all ends processing with
[L]before your rule is reached. Move yours to the top. - A leading slash in the pattern.
^/old-page$never matches in.htaccess; use^old-page$. - The query string is in the pattern. Use
RewriteCond %{QUERY_STRING}instead. - The browser cached an earlier redirect. Test with
curlor a private window. - Something in front answers first. A Cloudflare rule, a host-level redirect or a caching plugin serving an old copy.
How to check a 301 redirect
The redirect checker follows every hop from the address you enter to the page that answers, with each status code, and also tries the http:// and www variants. The result you want: one 301 (or 308) and then a 200.
For a list of old URLs, the bulk URL checker takes up to 1,000 at once and shows the final status, final URL and number of hops of each. From a terminal:
curl -sI https://www.example.com/old-page | grep -iE "^HTTP|^location"
curl -sL -o /dev/null -w "%{num_redirects} redirect(s), ended at %{url_effective} with %{http_code}\n" https://www.example.com/old-pageThe first should show 301 and the new Location; the second 1 redirect(s) ending with 200.
Common mistakes
- A 302 where you meant a 301. mod_rewrite without
R=301, a bareRedirect, Caddy'sredirwithoutpermanent. Check the status, not just that the redirect happens. - Chains. A new rule added on top of an old one that pointed at a page which has since moved. Update the old rule's target; redirect chains shows how to find them all.
- Loops. A → B and B → A, or Cloudflare's Flexible SSL plus an http → https rule on the server. ERR_TOO_MANY_REDIRECTS walks through the fixes.
- Everything to the home page. Google treats it as a soft 404, and visitors lose the page they wanted.
- Dropping the query string. Campaign links lose their
utm_tracking when the rule rebuilds the URL without it. - Leaving internal links on old URLs. Menus, content links and the sitemap should use the final URL.
Questions people ask
How do I do a 301 redirect?
It depends on where your site runs. On Apache, add RewriteRule ^old-page/?$ /new-page/ [R=301,L] to .htaccess; on nginx, location = /old-page { return 301 /new-page/; }; on WordPress, a plugin such as Redirection; on Cloudflare, a Redirect Rule; on Netlify, a line in _redirects. For many URLs, generate the rules from a two-column list, then test each one with a redirect checker.
How do I set up a 301 redirect in nginx?
Add a return 301 inside the server block, for example location = /old-page { return 301 https://example.com/new-page; }, then test the configuration with nginx -t and reload. For long lists, use a map in the http context and one return 301 that reads it. nginx does not read .htaccess files, so rules copied from Apache do nothing there.
How do I set up a 301 redirect in Cloudflare?
Use a Redirect Rule, under Rules, for a pattern such as an old folder or domain, with status 301 and a static or wildcard target. For long lists of exact URLs, use Bulk Redirects, which imports a CSV of source and target URLs. Cloudflare answers these at the edge before your server is reached, but only for host names with proxied (orange-cloud) DNS records.
Why is my .htaccess 301 redirect not working?
Usually the server is nginx (which ignores .htaccess), the rule sits below the WordPress block, the pattern starts with a slash, or your browser cached an earlier redirect. Also check that mod_rewrite is on and the server reads .htaccess at all. Move the rule to the top, use ^old-page/?$ without a slash, and test with curl or a private window.
How do I set up redirects on Netlify?
Create a file named _redirects in your site's publish folder and add one rule per line: the old path, the new path and the status, such as /old-page /new-page/ 301. Netlify uses 301 when you leave the status out, supports * with :splat for folders, and needs ! to redirect a path where a file still exists.