I recently moved a website from one domain to another. At first, the job looked trivial: add a 301 on the old server, bring the site up on the new server, and call it done.
The finished migration told a different story. A 301 is only the most visible link in a much longer chain. A new domain can appear healthy while deep URLs lose their paths, redirects take multiple hops, canonicals still point to the old host, APIs disappear behind a generic web-server template, analytics continue reporting under the old property, or custom error pages return a misleading 200 status.
This article is the playbook I wish I had at the start: how to make the server, application, search engines, analytics, and verification process agree on the same final URL.
Define success before changing anything
For this migration, “the new domain opens” was not enough. I used six acceptance criteria:
- HTTP, HTTPS, www, and non-www variants of the old domain all reach the new domain.
- Every old URL reaches its content-equivalent new URL without losing its path or query string.
- The move takes one permanent redirect, not a chain.
- Canonicals, hreflang links, internal links, feeds, and sitemaps all use the new domain.
- Static pages, APIs, feedback tools, certificates, and compression still work.
- Search engines and analytics platforms are explicitly updated.
Meeting only the first criterion means the new domain is online. It does not mean the migration is complete.
Start with a URL inventory, not a regular expression
The most valuable migration artifact is not the Nginx file. It is the mapping between old and new URLs.
If paths remain unchanged, the mapping is simple:
https://old.example.com/blog/article/
→ https://new.example.com/blog/article/
When paths change, make the mapping explicit:
https://old.example.com/post/123
→ https://new.example.com/blog/article-name/
Collect old URLs from sitemaps, access logs, search consoles, backlinks, and internal links. Track the result in a table:
| Old URL | New URL | Handling | Final status |
|---|---|---|---|
/blog/example/ |
/blog/example/ |
Domain-level 301 | 200 |
/post/123 |
/blog/example/ |
Explicit mapping | 200 |
/deleted-page/ |
No replacement | 404/410 | 404/410 |
Do not redirect every deleted page to the homepage merely to avoid visible dead links. Irrelevant redirects can be treated as soft 404s and confuse users as well.
Make the old host do exactly one job
For a domain-only move, a clear Nginx return 301 is usually enough:
server {
listen 80;
server_name old.example.com www.old.example.com;
return 301 https://new.example.com$request_uri;
}
server {
listen 443 ssl;
server_name old.example.com www.old.example.com;
ssl_certificate /etc/letsencrypt/live/old.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/old.example.com/privkey.pem;
return 301 https://new.example.com$request_uri;
}
Three details matter.
First, send old HTTP traffic directly to the new HTTPS URL. Avoid an unnecessary old-HTTPS intermediate hop.
Second, keep $request_uri. It preserves both the path and query string:
/tools/example/?from=old
→ https://new.example.com/tools/example/?from=old
Third, keep the old certificate valid. A browser must complete the TLS handshake with https://old.example.com before it can receive the redirect. An expired certificate breaks the journey before the 301 is visible.
Use explicit rules only for paths that actually changed. Prefer return for simple cases; reach for rewrite or map when capture and transformation are genuinely necessary.
Build the new Nginx config from the real service topology
The most dangerous mistake in my migration was nearly treating a generic PHP example as the new site’s template.
The real project is an Astro static site with APIs and a feedback service. Adding index.php, PHP-FPM, or /index.php?$args would not merely be unnecessary—it could break routing and hide missing backend locations.
A simplified static-site skeleton looks like this:
server {
listen 443 ssl;
server_name new.example.com;
root /var/www/site;
index index.html;
location /api/ {
proxy_pass http://127.0.0.1:8602;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
}
But even this is only a skeleton. A production host may also depend on more specific API locations, proxy_redirect for upstream redirects, sub_filter for root-relative links, precompressed gzip/Brotli assets, security headers, attachment routes, and carefully ordered location blocks.
The rule I now follow is simple:
The effective server configuration is the source of truth—not a template found online and not your memory of the old setup.
Inspect the complete configuration with nginx -T before simplifying anything.
Move every SEO signal together
Canonical URLs
Every indexable page on the new host should point to its new canonical URL:
<link rel="canonical" href="https://new.example.com/current-page/" />
A new-domain page whose canonical still points to the old domain is effectively declaring the old copy authoritative.
Hreflang
Multilingual sites must update language alternates as well:
<link rel="alternate" hreflang="zh-CN" href="https://new.example.com/page/" />
<link rel="alternate" hreflang="en" href="https://new.example.com/en/page/" />
<link rel="alternate" hreflang="x-default" href="https://new.example.com/page/" />
Alternates should be reciprocal, not one-way.
robots.txt
Update the sitemap declaration on the new host:
User-agent: *
Allow: /
Sitemap: https://new.example.com/sitemap-index.xml
Robots rules are scoped by scheme, host, and port. Redirecting the old /robots.txt with the rest of the site is fine, but the new host’s rules should not be described as globally governing every old-host crawl.
Sitemaps
Astro’s @astrojs/sitemap integration generates these files by default:
sitemap-index.xml
sitemap-0.xml
It does not necessarily create the /sitemap.xml path people often assume. A migration can therefore look complete while the URL submitted to every search console returns 404.
Keep only indexable, 200-status, canonical new-domain URLs in the sitemap. Google ignores priority and changefreq; include lastmod only when it is reliably accurate.
Tell search engines this is a move, not a copied site
For Google Search Console, verify both properties, activate the redirects, submit Change of Address from the old property, review all relevant www/non-www and subdomain variants, submit the new sitemap, and monitor indexing, crawl statistics, HTTPS, and search performance.
Google’s Change of Address notice runs for 180 days, while its site-move guide recommends keeping redirects for generally at least one year. Keeping them longer is often better for users and old backlinks.
For Baidu Search Resource Platform, establish the one-to-one redirects first, submit the domain or URL move through the site-revision tool, and submit the new sitemap. Interface names may change, so use the current console as the final authority.
Verify the new property in Bing Webmaster Tools as well. IndexNow can help announce newly added or updated URLs, but it does not replace old-to-new redirects.
Analytics can look healthy while still belonging to the old site
One particularly misleading symptom appeared after the move: the old Baidu Analytics property continued receiving new visits.
The redirect was not collecting those pageviews. A server-side 301 does not execute analytics JavaScript. The real reason was that the new site still loaded the old site’s tracking ID.
There are two reasonable choices.
Create a new analytics property for the new domain
This is the cleaner long-term option:
- Keep the old property for historical reporting.
- Add the new domain and obtain its new tracking ID.
- Replace the ID in the site and redeploy.
- Run the installation check and verify incoming data.
- Treat the migration date as the boundary between reports.
Ownership stays clear, though the trend line is split across two properties.
Keep the existing tracking ID
If a single continuous trend matters more, keep the ID, update the property details where possible, review domain filters, and annotate the migration date. This requires no code change, but the property may retain traces of its old-domain identity.
Avoid installing both IDs indefinitely unless you deliberately want a short parallel-validation period and understand that both properties will record the same visit.
Use a verification matrix, not “I opened the homepage”
At minimum, test all old-domain variants, a real deep page, a query string, the new homepage, www normalization, robots, sitemap, API health, and a deliberately missing URL:
curl -sSI http://old.example.com/
curl -sSI https://old.example.com/
curl -sSI http://www.old.example.com/
curl -sSI https://www.old.example.com/
curl -sSIL 'https://old.example.com/blog/real-article/?from=test'
curl -sSI https://new.example.com/
curl -sSI https://www.new.example.com/
curl -sSI https://new.example.com/robots.txt
curl -sSI https://new.example.com/sitemap-index.xml
curl -sS https://new.example.com/api/health
curl -sSI https://new.example.com/this-page-does-not-exist
The expected result is one redirect from each old URL to its final destination, preserved paths and queries, healthy infrastructure endpoints, and a genuine 404 for a missing page.
Use real URLs when expecting a final 200. A placeholder such as /post/123 proves nothing if that route never existed on the new site.
HEAD and GET checks are not enough for write workflows. Submit actual feedback, verify that it is stored, test post-write redirects, and load attachments through the public path.
Monitor the move in phases
| Period | What to watch |
|---|---|
| First 48 hours | Redirects, 404/5xx, certificates, APIs, CDN cache, server load |
| Weeks 1–4 | Crawling, indexing, search traffic, keywords, backlink visits |
| Months 2–3 | Old URLs leaving the index and new URLs stabilizing |
| Months 6 and 12 | Remaining old-domain traffic, backlinks, renewal, retention plan |
The most useful alerts are engineering facts: a non-301 response on the old domain, a redirect target returning 404/5xx, multiple hops, old/noindex/broken URLs in the sitemap, rising API errors, or failed renewal of the old certificate.
A domain move is really a signal-consistency project
The migration became much easier to reason about when I stopped treating it as a file-copy job.
- Nginx says the content moved permanently.
- Canonicals say the new URLs are authoritative.
- Hreflang keeps all language versions on the new domain.
- The sitemap submits only new canonical URLs.
- Search consoles receive an explicit move notification.
- Analytics adopts the new site identity.
- The old certificate and redirects remain healthy.
If one layer says the opposite, the move slows down and the redirect often gets blamed unfairly.
My final definition is this:
A domain migration is not copying a site to another server. It is getting users, browsers, search engines, analytics platforms, and external links to agree on one new canonical address.
When all five agree, the migration is truly complete.
References
- Google Search Central: Site Moves and Migrations
- Google Search Console: Change of Address tool
- Google Search Central: Redirects and Google Search
- Google Search Central: Build and Submit a Sitemap
- Google: robots.txt Specification
- Astro:
@astrojs/sitemap - Nginx: Rewrite Module /
return - Baidu Search Resource Platform
- Baidu Analytics Help