301 Redirects with .htaccess (Without Redirect Chains)
This tutorial is part of the SEO series for web developers. You'll need an Apache (or LiteSpeed) host that reads .htaccess files, which is most shared hosting.
When this site was restored from the Internet Archive, there were around 400 old URLs to account for: PHP pages from 2009, blog posts that had been captured under two different addresses, category pages with odd names, and links from other sites with typos in them. Every one of those needed to land on the right page, in one step, over https and on the www host. This post shows how that was done with a single .htaccess file.
Why a 301 and not something else?
A 301 Moved Permanently tells browsers and search engines that the old URL has a new permanent home. Search engines transfer the old URL's ranking signals, including the links pointing at it, to the new one. A 302 means "temporarily somewhere else" and is the wrong choice for a page that has really moved. Use 410 Gone when a page is removed on purpose and has no replacement. That is more honest than redirecting it to your home page, which search engines tend to treat as a "soft 404" anyway.
The basics
Turn on the rewrite engine and add one rule per old URL:
RewriteEngine On
RewriteBase /
# old PHP page → new clean URL
RewriteRule ^resume\.php$ https://www.example.com/resume/ [R=301,L]
# a whole folder that was renamed
RewriteRule ^blog/HTML5/(.+)$ https://www.example.com/blog/html5/$1 [R=301,L]
# removed on purpose, no replacement
RewriteRule ^surway/ - [G,L]
- The pattern after RewriteRule is a regular expression matched against the path without the leading slash.
- Escape dots (\.), otherwise . matches any character.
- [R=301,L] means "redirect with status 301, and stop processing rules".
- [G] returns 410 Gone.
The real problem: redirect chains
Most tutorials stop at the snippet above, and then add the usual "force https" and "force www" rules underneath it. That is how you end up with chains like this:
http://example.com/resume.php
→ 301 https://example.com/resume.php (force https)
→ 301 https://www.example.com/resume.php (force www)
→ 301 https://www.example.com/resume/ (the rule you actually wanted)
Each hop adds a round trip for visitors, and each hop is a chance for crawlers to give up or for link value to leak. The fix is to make every redirect go straight to the final URL, with the right scheme, the right host, the right path and the trailing slash, all in one hop. Order matters:
RewriteEngine On
RewriteBase /
DirectorySlash Off
# 1) specific legacy URLs first, with an ABSOLUTE https://www target
RewriteRule ^resume\.php$ https://www.example.com/resume/ [R=301,L]
# 2) folders requested without a trailing slash → add it (same hop)
RewriteCond %{DOCUMENT_ROOT}/$1 -d
RewriteRule ^(.*[^/])$ https://www.example.com/$1/ [R=301,L]
# 3) everything else: force https + www in one go
RewriteCond %{REQUEST_URI} !^/404\.html$
RewriteCond %{HTTPS} !=on [OR]
RewriteCond %{HTTP_HOST} !^www\.example\.com$ [NC]
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]
A few details in there are easy to miss:
- DirectorySlash Off stops Apache's own "add a slash" redirect from running after yours and creating a second hop. Rule 2 does the same job, but sends the visitor to the final URL.
- The 404.html exclusion matters because Apache serves your error page through an internal request. Without it, a 404 or 410 on the wrong host turns into a redirect to the error page.
- Don't use the [NE] flag on rules that copy the path with $1. If a file name contains a space, NE writes it into the Location header unescaped.
Test every URL in every form
A redirect map is only as good as its testing. For each important URL, request all four forms (http/https × www/no-www) and check that the answer is either a 200, or exactly one 301 whose target returns 200:
for u in http://example.com/resume.php https://example.com/resume.php \
http://www.example.com/resume.php https://www.example.com/resume.php; do
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" "$u"
done
# every line should read: 301 https://www.example.com/resume/
You can run the same test before you launch by pointing curl --resolve at your new server's IP while DNS still points at the old one. That is how this site's 400-odd legacy URLs were checked before the switch.
What not to do
- Don't redirect everything to the home page. It is tempting when you have hundreds of old URLs, but visitors land somewhere unrelated, and search engines treat those redirects as errors. Send each URL to the closest page on the same topic, or return 410.
- Don't change URLs that have backlinks unless you really have to. A redirect keeps most of the value. Keeping the original URL keeps all of it.
- Don't forget non-HTML files. Old PDFs, images and stylesheets get linked too.
Redirects are the riskiest part of moving or rebuilding a site. If you are moving a larger site, such as a product catalogue with thousands of URLs, Southern Whale describes how they plan and test that kind of move here: Website Migration & Technical SEO Migration.