Moving a Next.js Site to a New Domain: What Broke, and the Checklist I Use Now
I moved a live Next.js 16 site to a new domain on Vercel in one afternoon. The 301s worked on the first try. The canonicals, robots.txt and sitemap did not. Here is why, and the checks that catch it." tags: nextjs, seo,
I moved a live Next.js 16 site to a new domain on Vercel in one afternoon. The 301s worked on the first try. The canonicals, robots.txt and sitemap did not. Here is why, and the checks that catch it."
tags: nextjs, seo, webdev, vercel
On 19 September 2026 I moved my digital products shop from toolgenx.com to agentskillpacks.com. Same Next.js 16 app, same Vercel project, same brand. New domain, because the old name said nothing about what the site sells.
The redirect part took ten minutes and worked on the first try. The part that bit me was everything the site says about itself: canonical tags, robots.txt, the sitemap, JSON-LD @ids. For a while the new domain was serving pages that told Google the real address was the old one.
This post is what broke, why, and the checklist I now run for any domain move.
TL;DR
- Redirects are the easy part. The dangerous part is self-referencing output: canonicals,
robots.txtHost/Sitemaplines,sitemap.xml<loc>s,og:url, and structured data@ids. - A stale
NEXT_PUBLIC_SITE_URLin Vercel silently kept all of those on the old host after the move. I now pin the production origin in code. -
grepfor the old domain misses URL-encoded variants such as%20toolgenx.cominsidemailto:subjects and share links. Grep the build output, not just the source. - Verify on the live new host with
curl, not in the browser, and check that 301s keep both the path and the query string.
What actually has to change in a domain move?
A domain move changes two different things. Redirects move visitors and crawlers from the old host to the new one. Self-references tell crawlers which host is the real one: canonical URLs, sitemap entries, robots.txt directives, Open Graph URLs and structured data identifiers. Redirects without matching self-references send search engines mixed signals.
In a typical Next.js App Router project, self-references come from a handful of places:
-
metadataBasein the root layout (drivesalternates.canonicalandopenGraph.url) -
app/sitemap.tsandapp/robots.ts(or route handlers that replace them) - JSON-LD builders (
Organization,WebSite,Product@idandurl) - Anything else that builds absolute URLs: RSS feeds,
llms.txt,Linkheaders, email templates, share buttons
Most of these read one value: "what is my origin?". That value is where things went wrong.
Mistake 1: the origin lived in an environment variable
My origin came from NEXT_PUBLIC_SITE_URL, with a sensible default. I swapped every hardcoded URL in the source (28 files), ran the tests, built locally against the new host, and deployed.
Production still said the old domain. The Vercel project had NEXT_PUBLIC_SITE_URL=https://www.toolgenx.com set from years ago, and it overrode the new default. The result on the live new host:
-
robots.txtpointed itsSitemap:line at the old host - every
<loc>in the sitemap used the old host - every canonical tag pointed at the old host
-
OrganizationandWebSite@ids used the old host
And the old host was already 301-redirecting to the new one. So Google was being told "the real page is over there", and "over there" redirected back. Not a loop that breaks the site, but exactly the kind of contradiction that slows a migration down.
The fix was to stop treating the production origin as configuration:
// src/lib/env.ts
// The public origin is a constant, not an env var. A stale Vercel value once
// kept canonicals, robots.txt and the sitemap on the old domain after the move.
const SITE_URL =
process.env.NODE_ENV === 'production'
? 'https://www.agentskillpacks.com'
: 'http://localhost:3000';
A production origin changes maybe once in a site's life. When it does, you want the change in a commit with a message, reviewed and visible in the diff, not in a dashboard nobody remembers.
Lesson: after a domain move, list every env var in the hosting dashboard that contains the old domain. Delete or update each one before you deploy.
Mistake 2: grep found every URL except the encoded ones
Before deploying I grepped the repo for the old domain and filtered out email addresses (support email stays on the old domain for now). Clean.
Then I spot-checked the built HTML and found toolgenx.com still inside mailto: links, where the subject line was URL-encoded:
mailto:support@...?subject=Broken%20link%20on%20toolgenx.com
My "exclude emails" filter saw the @ and skipped the whole line. Share links have the same problem (https%3A%2F%2Fwww.toolgenx.com).
The reliable check is to grep the build output for every encoding of the old host:
# after `next build`
grep -rEo "toolgenx\.com|toolgenx%2Ecom|%2F%2Fwww\.toolgenx" .next/server/app \
| sort | uniq -c | sort -rn | head -20
Anything left is either intentional (an email address you are keeping) or a bug.
Mistake 3: trusting the browser instead of curl
The browser follows redirects, caches aggressively and hides headers. For a migration you want the raw response. This is the script I now run against the new host right after the deploy:
#!/usr/bin/env bash
NEW="https://www.agentskillpacks.com"
OLD="https://www.toolgenx.com"
OLD_HOST="toolgenx.com"
echo "== robots.txt (Host / Sitemap lines)"
curl -s "$NEW/robots.txt" | grep -Ei "^(host|sitemap):"
echo "== sitemap <loc> hosts"
curl -s "$NEW/sitemap.xml" | grep -o "<loc>[^<]*" | sed 's/<loc>//' \
| awk -F/ '{print $3}' | sort | uniq -c
echo "== canonical + og:url on key pages"
for p in / /products /blog; do
html=$(curl -s "$NEW$p")
echo "$p"
echo "$html" | grep -o '<link rel="canonical"[^>]*>'
echo "$html" | grep -o '<meta property="og:url"[^>]*>'
done
echo "== old host still mentioned in live HTML?"
curl -s "$NEW/" | grep -c "$OLD_HOST"
echo "== 301 keeps path and query"
curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" \
"$OLD/blog/some-post?utm_source=test"
What "good" looks like:
-
robots.txthas exactly oneSitemap:line on the new host - every sitemap
<loc>is on the new host (mine: 80 of 80) - canonical and
og:urlon the new host for every page you check - the 301 returns
301 -> https://www.agentskillpacks.com/blog/some-post?utm_source=test, with path and query intact
That last one matters more than it looks. Redirects that drop the path send every old deep link to the homepage, and redirects that drop the query break campaign tracking.
Mistake 4 (avoided): forgetting the things outside the repo
Some parts of a migration are not in the codebase at all. The ones I track:
- Search Console Change of Address. It requires the old property to be verified. If you verified the old domain with a DNS TXT record, keep that record until the change of address is done.
- Bing Webmaster Tools has its own site move tool.
- IndexNow works per host: the key file has to be served on the new host too. Mine was served by the app, so the same key worked, and I submitted all 80 URLs once after the move.
- Affiliate networks, analytics and pixels often verify by domain and need re-verification.
- External profiles (Product Hunt, Crunchbase, GitHub, social bios) still link to the old domain. The 301 covers them, but updating them removes a hop.
The checklist
Before the switch:
- [ ] Search the source for the old domain in every encoding (
.,%2E,%2F%2F) - [ ] List and update every hosting env var that contains the old domain
- [ ] Pin the production origin in code, with localhost for dev and tests
- [ ] Decide explicitly what keeps the old domain (email addresses) and why
Deploy:
- [ ] Add the new domain in Vercel, make it primary, redirect the old host with 301
- [ ] Build, then grep the build output for the old host
After the switch (on the live new host, with curl):
- [ ]
robots.txtSitemap:line on the new host - [ ] every sitemap
<loc>on the new host - [ ] canonical,
og:urland JSON-LD@idon the new host - [ ] 301 keeps path and query string
- [ ] Search Console Change of Address, Bing site move, IndexNow ping
- [ ] re-verify analytics, affiliate and ad accounts
Does a keyword domain help SEO?
Not directly. An exact-match domain is a weak ranking signal on its own in 2026, and a move always costs some time while search engines reprocess the site. I moved because the new name describes what the shop sells, which helps people and AI assistants understand the site at a glance, and because the old domain was young enough that there was little ranking history to lose.
If your domain has years of links and rankings, the bar for moving should be much higher.
FAQ
How long does a domain migration take to settle in Google?
There is no fixed number. Redirects and canonicals are read on the next crawl, but full consolidation of rankings to the new host can take weeks to months, depending on site size and crawl frequency. Clean self-references shorten it; contradictions stretch it.
Should canonical tags point to the new domain before the redirect is live?
Switch them in the same deploy that makes the new domain primary. Canonicals pointing at a host that is not serving yet, or at a host that redirects away, both send conflicting signals.
Can I keep email on the old domain?
Yes. Email runs on MX records, not on the website. Just make sure the website does not keep linking to the old host in places a crawler reads.
Do I need to resubmit the sitemap?
Submit the new sitemap in the new Search Console property and use Change of Address from the old one. An IndexNow ping helps Bing and other participating engines pick up the new URLs quickly.
If you want the robots.txt side of this in more detail, I wrote up every line of this site's robots.txt and llms.txt and why it is there. And if you want to check which AI crawlers your own robots.txt lets in after a move, there is a free AI crawler checker on the site. No signup.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.