Redirect a Whole Site to a New Domain with .htaccess

301-redirect every page from the old domain to the new one — path-preserving rules, the HTTPS certificate trap, and the Change of Address checklist.

The job: every request that arrives at the old domain should permanently land on the same path at the new one — /a/b?x=1 → https://new.com/a/b?x=1 — in a single 301 hop. Two Apache directives do it; the choice between them is about what else is in the file.

Option A — mod_alias, two lines

# Whole-site move — mod_alias appends the unmatched path automatically.
Redirect 301 / https://new.com/

Redirect is a prefix match: the source / matches every request, and mod_alias carries the remainder of the path — and the query string — onto the target. /a/b?x=1 arrives as https://new.com/a/b?x=1. No $1, no regex, nothing to get wrong. This is exactly what the generator emits when you add a whole section row with source / (it uses the RedirectMatch equivalent: RedirectMatch 301 ^(/.*)?$ https://new.com$1).

The catch is ordering: in .htaccess, mod_rewrite runs before mod_alias regardless of where the lines sit in the file. If the file also contains rewrite rules — a WordPress front controller, canonicalization — those rules see the request first. An internal rewrite doesn’t stop the mod_alias redirect from firing afterwards, but a rewrite rule that emits its own R=301 does end processing. For a file that contains only this redirect, mod_alias is the clean answer; for a file that already has RewriteRules, use option B so ordering is explicit.

Option B — mod_rewrite, with a self-loop guard

<IfModule mod_rewrite.c>
RewriteEngine On
# Fire for anything that is NOT already the new host — without this line,
# a request to new.com loops when both domains share this docroot.
RewriteCond %{HTTP_HOST} !^new\.com$ [NC]
RewriteRule ^(.*)$ https://new.com/$1 [R=301,L]
</IfModule>

Two details carry the weight here:

  • %{HTTP_HOST} !^new\.com$ [NC] — the guard. During a migration both domains usually point at the same directory while DNS propagates. Without the condition, https://new.com/x would redirect to https://new.com/x forever. With it, requests already on the new host pass straight through.
  • [R=301,L] — 301 (permanent: ranking signals transfer, browsers cache it) and L stops further rewriting this pass. If the same file forces HTTPS or canonicalizes a host, keep the domain-move rule first — a visitor on http://old.com should jump straight to https://new.com, not bounce old→https-old→new in two hops. Better still, merge the tests with [OR] exactly as the HTTPS + canonical guide shows: RewriteCond %{HTTPS} off [OR] plus the host condition, one rule, one hop.

The certificate trap

For https://old.com/… requests the TLS handshake completes before Apache reads .htaccess. If the old certificate lapses, visitors get the browser warning page and never see the redirect at all. The old domain must keep a valid certificate — most hosts auto-renew — for as long as you keep the redirects. HTTP requests redirect fine either way, but most inbound links and all HSTS primed clients arrive on HTTPS.

The checklist that makes it stick

  1. Verify both properties in Google Search Console, then file Settings → Change of address on the old property — it tells Google the move is deliberate and speeds signal transfer.
  2. Preserve paths, not just the host. Everything-to-homepage redirects get treated as soft-404s.
  3. WordPress specifically: update siteurl/home (Settings → General, or wp option update) or WP will redirect visitors back to the old domain — a loop against your own .htaccess.
  4. Subdomains: the %{HTTP_HOST} rule folds every host that reaches this directory into new.com. If blog.old.com or status.old.com should survive, exempt them with an extra !^host$ condition — the same pattern the generator’s canonical block warns about in its comments.
  5. Keep paying for the old domain. Expired registration = dead redirects = the migration never happened as far as search engines are concerned.

Verify before you call it done

curl -I http://old.com/deep/page?keep=1        # expect: 301 → https://new.com/deep/page?keep=1
curl -I https://old.com/another?x=1            # same, on TLS
curl -I https://new.com/deep/page              # expect: 200 — no loop

Test with curl, not a browser — a cached 301 makes a broken fix look done and a done fix look broken. If the redirect “doesn’t fire” in curl, walk the 500-error checklist (mod_alias needs FileInfo in AllowOverride) and the 301 guide for chains and loops.

Frequently asked questions

Do redirects work if the old domain's HTTPS certificate expired?

No — and this is the trap that bites most migrations. For an https://old.com request, the TLS handshake finishes before Apache reads .htaccess. An expired cert means the browser shows a warning page and the redirect never happens. Keep the old domain's certificate auto-renewing for as long as the redirects live.

Should I redirect every page to the new homepage instead?

No. Mass-redirecting deep URLs to the homepage is what Google classifies as a soft-404 — the old page's ranking signals evaporate instead of transferring. Preserve the path: /a/b?x=1 should land on https://new.com/a/b?x=1. Both rules above do that automatically.

How long do the redirects need to stay up?

At minimum several months, realistically a year or more. Search engines re-crawl old URLs on their own schedule, and browsers cache 301s permanently — a visitor who got the redirect once may never ask the old domain again. Keep the domain registered, the certificate valid, and the rule in place until old-domain traffic flatlines in your logs.

Does moving the domain break email?

No. .htaccess only touches HTTP requests — MX records, mailboxes and SPF/DKIM live in DNS and are unaffected. The only thing to keep is the DNS record itself: the old domain must still resolve to the server hosting this .htaccess for redirects to fire.

Can I keep one subdirectory on the old domain?

Yes — exempt it before the catch-all: RewriteRule ^keep-this(/|$) - [L] placed above the redirect rule leaves that subtree served locally while everything else moves. The same idea excludes a hostname: another RewriteCond %{HTTP_HOST} !^sub\.old\.com$ [NC] line spares a subdomain that should stay.