# Bulk 301 Redirects in .htaccess: A Step-by-Step Guide

*Originally published on the [MatrixInn Solutions blog](https://matrixinnsolutions.com/blog/bulk-301-redirects-htaccess).*

When you change URLs — a redesign, a new CMS, merging blog posts, moving to a new domain — every old URL that isn't redirected turns into a 404. Visitors hit a dead end, and the ranking signals that page earned stay stuck on a URL that no longer exists. A **301 redirect** tells browsers and search engines the page has moved permanently and passes those signals to the new address.

Redirecting one page is easy. Redirecting 200 is where people make mistakes. Here's a process that works for any size of migration.

## Step 1: Build your redirect map in a spreadsheet

Make two columns: **old path** and **new URL**. One row per page.

| Old path | New URL |
|---|---|
| /services/web-design.html | https://example.com/services/web-design |
| /blog/2019/05/my-post | https://example.com/blog/my-post |
| /shop/blue-jacket | https://example.com/products/blue-jacket |

Where to get the old URLs:
- Your **old sitemap.xml** (the best source)
- **Google Search Console → Pages**, and the Performance report sorted by clicks. Prioritize URLs that actually get traffic
- A crawl of the old site with Screaming Frog or a similar crawler

Map every old URL to its **closest equivalent**, not to the homepage. Google treats mass redirects to the homepage as soft 404s, and the ranking signals are lost anyway.

## Step 2: Generate the rules

For simple one-to-one moves, Apache's `mod_alias` is the cleanest option:

```apache
Redirect 301 /services/web-design.html https://example.com/services/web-design
Redirect 301 /blog/2019/05/my-post https://example.com/blog/my-post
Redirect 301 /shop/blue-jacket https://example.com/products/blue-jacket
```

If your `.htaccess` already uses `RewriteRule` (WordPress, most PHP frameworks), keep everything in `mod_rewrite` so the two modules don't fight over the same request:

```apache
RewriteEngine On
RewriteRule ^services/web-design\.html$ https://example.com/services/web-design [R=301,L]
RewriteRule ^blog/2019/05/my-post$ https://example.com/blog/my-post [R=301,L]
RewriteRule ^shop/blue-jacket$ https://example.com/products/blue-jacket [R=301,L]
```

Notice the escaped dot (`\.html`) and the `^…$` anchors. Without them, `/shop/blue-jacket` would also match `/shop/blue-jacket-xl`.

Writing hundreds of these by hand is slow and error-prone, and one stray space can return a 500 error for the whole site. Instead, copy your two spreadsheet columns into our free **[.htaccess redirect generator](https://tools.matrixinnsolutions.com/htaccess-redirect-generator)**, open **Bulk paste**, and click **Import & generate**. It trims full URLs down to paths, escapes regex characters, and outputs either `Redirect` or `RewriteRule` lines. It all runs in your browser, so your URL list is never uploaded.

## Step 3: Put the rules in the right place

1. **Back up** your current `.htaccess`.
2. Paste the redirects **near the top**, after `RewriteEngine On` but **before** any catch-all rules (like WordPress's `# BEGIN WordPress` block). Apache processes rules top to bottom, and a catch-all placed earlier will swallow your redirects.
3. Put more specific paths above broader patterns.

## Step 4: Avoid the three classic mistakes

**Redirect chains.** If `/a` already redirects to `/b`, and you now move `/b` to `/c`, update the first rule to go straight to `/c`. Every extra hop slows the page down and loses a little signal. Keep every redirect to a single hop.

**Redirect loops.** A rule whose destination matches its own pattern (or another rule that points back) loops forever and the browser shows "too many redirects". A common version of this: a rule that strips trailing slashes fighting Apache's own `DirectorySlash`, which adds them back to real folders. Test folder URLs specifically.

**Using 302 by accident.** `R` on its own in a `RewriteRule` means **302** (temporary). For permanent moves always write `R=301`.

## Step 5: Test before you trust it

Check a sample of old URLs from the command line:

```bash
curl -I https://example.com/blog/2019/05/my-post
```

You want `HTTP/1.1 301 Moved Permanently` and a `Location:` header with the new URL, and that new URL should return `200`, not another redirect. For a big list, loop through your old paths in a script, or run a crawler in list mode over the old URLs.

Then in Google Search Console:
- Submit the **new** sitemap.
- Watch **Pages → Not found (404)** over the next few weeks for anything you missed, and add a redirect for each one.

## How long should redirects stay?

Keep them for **at least a year**, and ideally permanently. Other websites, bookmarks and old social posts keep linking to your old URLs long after Google has updated its index. Removing redirects early throws that traffic away.

## Quick checklist

- [ ] Every old URL with traffic or backlinks is in the map
- [ ] Each maps to its closest new equivalent (not the homepage)
- [ ] One module style (`Redirect` *or* `RewriteRule`), placed before catch-all rules
- [ ] `R=301`, anchored patterns, escaped dots
- [ ] No chains, no loops, and folder URLs tested
- [ ] New sitemap submitted; 404 report checked weekly

Need to generate the rules now? Paste your list into the **[free bulk .htaccess redirect generator](https://tools.matrixinnsolutions.com/htaccess-redirect-generator)**. Planning a bigger migration or rebuild? [Talk to us](https://matrixinnsolutions.com/start).


