# How to do a 301 redirect on Apache, nginx, Cloudflare, WordPress, Netlify and Vercel

> How to do a 301 redirect wherever your site runs: copy-ready rules for Apache, nginx, Cloudflare, WordPress, Netlify and Vercel, for one page or a whole list, and how to test them.

Updated 2026-09-27 · Technical SEO · HTML version: https://getreport.app/guides/301-redirects-htaccess-nginx-cloudflare-wordpress

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](https://getreport.app/guides/301-redirects) 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 the `server` block, then `nginx -t` and 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 WordPress` for speed.
- **Netlify:** `/old-page /new-page/ 301` in `_redirects`. **Vercel:** a `redirects` entry in `vercel.json`.
- **Many URLs:** list old and new in two columns and generate the rules with the [redirect rule generator](https://getreport.app/tools/redirect-generator).
- **Test** with the [redirect checker](https://getreport.app/tools/redirect-checker) or `curl -sI`: one 301, then a 200. Say 301 explicitly: several tools send a 302 by default, which [301 vs 302](https://getreport.app/guides/redirects-without-chains) 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:

```http
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](https://getreport.app/guides/404-vs-410-vs-redirect-what-to-do-with-removed-pages) 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:**

```apache
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:**

```apache
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:

```apache
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:**

```nginx
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:**

```nginx
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`:

```nginx
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:

```nginx
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](https://getreport.app/guides/trailing-slashes-www-and-https-pick-one).

## 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"`, URL `https://www.example.com/new-page/`, status **301**, "Preserve query string" on.
- **Pattern:** use the wildcard fields, request URL `https://www.example.com/shop/*` and target `https://www.example.com/store/${1}`, which moves `/shop/mugs` to `/store/mugs`. Older rules written as expressions with `concat()` 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:

```text
www.example.com/old-page,https://www.example.com/new-page/,301
www.example.com/blog/2019/summer-sale,https://www.example.com/offers/,301
```

A 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](https://getreport.app/guides/wordpress-redirects) 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:

```text
/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):

```json
{
  "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](https://getreport.app/guides/redirect-mapping-for-site-migration) covers collecting and matching them.

> **Free tool:** [301 redirect .htaccess and nginx rule generator](https://getreport.app/tools/redirect-generator): Get 301 redirect .htaccess rules, or nginx, Cloudflare, Netlify, Vercel and Caddy rules, from your list of old and new URLs. Free, runs in your browser.

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](https://getreport.app/guides/redirect-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:

1. **The server is not Apache.** nginx ignores `.htaccess` completely. LiteSpeed reads it; nginx-based hosts need the rule in their panel or config.
2. **`.htaccess` is not read.** With `AllowOverride 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.
3. **mod_rewrite is off.** Enable it (`a2enmod rewrite` and a restart) or ask the host.
4. **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.
5. **A leading slash in the pattern.** `^/old-page$` never matches in `.htaccess`; use `^old-page$`.
6. **The query string is in the pattern.** Use `RewriteCond %{QUERY_STRING}` instead.
7. **The browser cached an earlier redirect.** Test with `curl` or a private window.
8. **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

> **Free tool:** [Redirect checker: 301, 302 and redirect chains](https://getreport.app/tools/redirect-checker): Free redirect checker: follow every hop from your URL to the final page, with status codes, HTTP to HTTPS and www redirects, and the canonical at the end.

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.

> **Check: The URL loads without a redirect chain.** Each redirect is a full request and response before the browser can start loading the page, which slow mobile connections feel most. Google says redirects do not lose ranking value, but Googlebot follows at most 10 hops, and long chains slow crawling and break when one hop changes.
>
> 1. Point every old URL straight at the final one (a single 301), and update internal links to use the final URL directly.
> 2. Look for rules that stack, for example http → https, then non-www → www, then trailing slash; combine them into one rule.

> **Check: http:// redirects to https://.** Visitors who type your address without https, or follow an old link, land on the unencrypted page. Every one of those visits can be read or altered on the network.
>
> 1. Redirect every http:// URL to the same path on https:// with a 301.
> 2. nginx: return 301 https://$host$request_uri; Apache: RewriteRule in .htaccess; most hosts and Cloudflare have a "Always use HTTPS" switch.

> **Check: www and non-www redirect.** When www and non-www both answer, visitors and tools see two sites with separate cookies, caches and analytics sessions. One should redirect to the other.
>
> 1. Pick one host and 301-redirect the other to it, keeping the path; most hosts and CDNs have a one-click setting for this.
> 2. Use the chosen host everywhere (links, sitemap, analytics property, social profiles).

> **Check: No redirects before the page.** Every redirect is a full round trip before the browser can even ask for the page. http → https → www → trailing slash can easily add a second on mobile.
>
> 1. Link straight to the final URL everywhere (menus, ads, social profiles).
> 2. Collapse chains: redirect http and non-www directly to the final https URL in one hop.

For a list of old URLs, the [bulk URL checker](https://getreport.app/tools/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:

```bash
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-page
```

The 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 bare `Redirect`, Caddy's `redir` without `permanent`. 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](https://getreport.app/guides/redirect-chains-after-a-migration-finding-every-hop) 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](https://getreport.app/guides/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.
