You moved from oldsite.com to newsite.com, pointed DNS over, and the new domain loads — but view-source on a production page still shows <link rel="canonical" href="https://oldsite.com/...">. Left alone, Google keeps treating the old domain as primary and your search traffic never fully moves.
Fastest fix: the canonical is almost always baked in at build time from a hard-coded site URL, then frozen by a CDN cache. Grep your repo for the old domain, replace it with one env var (SITE_URL), rebuild, redeploy, purge the CDN cache, and confirm with curl. The Search Console “Change of address” step is what tells Google about the move — do it, but it does not rewrite your HTML.
This is rarely one root cause. It’s a stack: hard-coded URLs + CDN cache + stale sitemap/301s + Google not having re-crawled yet. Peel them off in order.
Which bucket are you in?
| Symptom you can observe | Most likely layer | Jump to |
|---|---|---|
curl -s https://newsite.com/page | grep canonical shows oldsite, even on a fresh deploy | Hard-coded site URL in source | Step 1 |
Source is fixed in dist/ but production still serves old canonical; curl -I shows a cache HIT | CDN / host cache | Step 3 |
sitemap.xml still lists oldsite.com <loc> entries | Sitemap / robots not updated | Step 4 |
oldsite.com 301s to new, but new pages canonical back to old | 301 ↔ canonical loop | Step 4 |
| HTML is correct everywhere, GSC still shows old domain as canonical | Google hasn’t re-processed | Step 5 |
In Search Console’s Pages report (formerly “Index Coverage”), the matching statuses to look for are Duplicate, Google chose different canonical than user (serious — Google is overriding your canonical, usually because the old URL still has more links/sitemap weight) and Duplicate without user-selected canonical. The status Alternate page with proper canonical tag is informational and usually fine — it means Google saw your canonical and honored it.
Common causes, ordered by hit rate
1. Site root URL hard-coded in source
astro.config.mjs / next.config.js / templates have a literal https://oldsite.com that gets baked into every page’s canonical, og:url, and sitemap.xml at build time. Even after the host domain changes, the HTML still ships the old one.
Typical:
// astro.config.mjs
export default defineConfig({
site: 'https://oldsite.com', // ← never updated
});
How to spot it: grep -r "oldsite.com" src/ astro.config.mjs next.config.js public/ and count the hits.
2. CDN / host is serving cached HTML
Code is fixed and redeployed, but Cloudflare / Vercel Edge / Fastly is still serving the previous HTML. curl -I shows cf-cache-status: HIT or x-vercel-cache: HIT — you’re seeing the cache, not your new build.
How to spot it: curl -s https://newsite.com/some-page | grep canonical still shows oldsite, and curl -I shows a cache HIT.
3. Sitemap and robots.txt aren’t updated
sitemap.xml still lists https://oldsite.com/... URLs. Once submitted, Google keeps crawling the old URLs, and the non-canonical URLs in your sitemap actively push Google toward picking the old domain. A robots.txt line pointing at an absolute sitemap URL on the old domain has the same effect.
How to spot it: curl https://newsite.com/sitemap.xml | head -50 and check the <loc> entries.
4. 301 redirect is only half-configured
oldsite.com 301s to newsite.com are in place, but pages on the new domain still canonical back to oldsite.com. Google then sees 301 -> canonical -> 301 pointing in a circle and refuses to settle on either URL.
How to spot it: curl -I https://oldsite.com/page shows the 301 target, then curl https://newsite.com/page | grep canonical shows the canonical. If they point at each other, you have a loop.
5. Search Console “Change of address” not submitted
The Change of address tool is what tells Google the move is permanent at the whole-site level — 301s alone only carry per-URL signals. Two gotchas as of June 2026: the tool only works between two Domain properties (not URL-prefix properties), and you must be an owner of both the old and new properties in the same Google account.
How to spot it: in Search Console, open the old property → Settings → Change of address. If it’s greyed out or missing, your property is probably a URL-prefix property, or you don’t own the new one yet.
Shortest path to fix
Step 1: Grep the old domain and switch to a single env var
grep -rIn "oldsite\.com" . \
--exclude-dir=node_modules \
--exclude-dir=.git \
--exclude-dir=dist
Replace every hard-coded occurrence with an env var:
// astro.config.mjs
export default defineConfig({
site: process.env.SITE_URL || 'https://newsite.com',
});
// src/lib/seo.ts
export const SITE_URL = import.meta.env.SITE_URL || 'https://newsite.com';
// In templates
<link rel="canonical" href={`${SITE_URL}${Astro.url.pathname}`} />
Every new-domain page should carry a self-referencing canonical (the page points to its own new-domain URL). Google’s site-move guidance is explicit that once redirects are live, the new pages’ rel="canonical" must use the new URLs — a page that canonicals to any other URL during a move is the single fastest way to confuse the migration.
Set SITE_URL=https://newsite.com on the host (Vercel / Netlify / Cloudflare) for both Preview and Production. Mismatched values between environments are a common reason the fix “works locally but not in prod”.
Step 2: Rebuild, redeploy, verify the artifact
# Verify locally first
npm run build
grep -rIn "oldsite.com" dist/ || echo "clean"
# Verify in production after deploy
curl -s https://newsite.com/ | grep -E '<link rel="canonical"|og:url'
Every match should be https://newsite.com/.... If dist/ is clean but production still shows the old domain, you’re hitting the cache — go to Step 3.
Step 3: Purge CDN / host cache
- Cloudflare: dashboard → Caching → Configuration → Purge Everything (confirm in the warning dialog).
- Vercel: Deployments → latest → Redeploy with “Use existing Build Cache” unchecked. From the CLI,
vercel --force, or setVERCEL_FORCE_NO_BUILD_CACHE=1. Vercel’s edge cache is keyed to each deployment, so a fresh deploy serves fresh HTML. - Cloudflare Pages: Settings → Build → clear the build cache, then re-deploy.
- Netlify: Deploys → Trigger deploy → Clear cache and deploy site.
Then curl -I https://newsite.com/some-page should show cf-cache-status: MISS (or EXPIRED) on the next-to-first request, and curl should return the new canonical.
Step 4: Update sitemap, robots, and 301 redirects
public/robots.txt:
User-agent: *
Allow: /
Sitemap: https://newsite.com/sitemap.xml
Configure a full-path 301 from old to new on the old domain (pick one — Cloudflare Page Rule / Vercel vercel.json / Nginx):
// vercel.json (on the oldsite project)
{
"redirects": [
{ "source": "/(.*)", "destination": "https://newsite.com/$1", "permanent": true }
]
}
Verify the redirect preserves the path (a homepage-only redirect is a common mistake that drops every deep link):
curl -I https://oldsite.com/some-page
# HTTP/2 301
# location: https://newsite.com/some-page
Step 5: Submit “Change of address” in Search Console
- Add
https://newsite.comas a property in GSC and verify it. Use a Domain property for both old and new so the Change of address tool is available. - Implement the homepage 301 first — the tool runs pre-move checks and will flag a missing
oldsite.com -> newsite.com301 on the home page as a critical failure. - In the old property, go to Settings → Change of address, pick the new property, and submit. Fix any critical pre-move check before continuing; non-critical checks are warnings you can pass.
- Submit the new sitemap (
https://newsite.com/sitemap.xml) under the new property. - For your most important pages, use URL Inspection → “Request indexing” to nudge a re-crawl. The manual quota is small (roughly 10–12 URLs per property per day as of June 2026, on a rolling 24-hour window), so prioritize a handful and let the sitemap carry the rest.
Google typically takes a few weeks for a small-to-medium site to move most pages, longer for large sites, and ranking fluctuations during the recrawl are normal. Keep the old domain’s 301s live the entire time — Google’s site-move guidance (as of June 2026) is at least 180 days, and “as long as possible, generally at least 1 year,” plus longer still if you keep seeing Search traffic on the old URLs.
How to confirm it’s fixed
curl -s https://newsite.com/ | grep canonicalreturns onlyhttps://newsite.com/...— no oldsite anywhere.curl -I https://newsite.com/shows a fresh response (cf-cache-status: MISS/EXPIRED, or a newx-vercel-id).curl -I https://oldsite.com/deep/pathreturns301to the same deep path on the new domain.- In GSC, run URL Inspection on a new-domain URL → “Google-selected canonical” matches your declared canonical.
- Over the following weeks, the old domain’s impressions in GSC trend down while the new domain’s trend up.
FAQ
How long until Google stops showing the old domain? For a small-to-medium site, expect a few weeks for most pages to move and up to a few months to fully settle, even with everything configured correctly; larger sites take longer. The Change of address signal and 301s speed it up but don’t make it instant. Keep redirects live at least 180 days — Google now phrases it as “generally at least 1 year.”
The HTML is correct but GSC still says the canonical is the old domain. Why?
Google caches its own crawl. If the current HTML, sitemap, and 301s are all consistent, this resolves on the next crawl. If it persists, check that the URL Google picked isn’t being fed by a stale sitemap entry or by more inbound links to the old URL — that’s the Duplicate, Google chose different canonical than user case.
Do I even need the Change of address tool if I have 301s? 301s carry per-URL signals; Change of address adds a site-wide “this is a permanent move” signal and speeds consolidation. Use both. Note: it does not apply to an HTTP→HTTPS move (follow the standard site-move guidance for that), and it requires Domain properties you own on both sides.
I purged the cache but still see the old canonical.
Confirm the build artifact is actually clean first (grep -rIn "oldsite.com" dist/). If dist/ is clean and curl -I shows a MISS but the body is still old, you may have a second cache layer (e.g. Cloudflare in front of Vercel) — purge both, and check for a service worker caching pages in the browser.
Should I keep the old domain forever? No need, but keep it and its 301s for at least 180 days, and ideally a full year (Google’s own wording is “as long as possible, generally at least 1 year”), so Google fully consolidates ranking signals before you let the domain lapse. 301s do not cost you PageRank, so there is no downside to leaving them up longer.
Prevention
- Read the site root URL from one env var only; ban any literal domain string from source.
- Add a CI rule:
grep -r "oldsite.com" src/returning a match fails the build. - After every big change (domain, route shape, canonical template), run a canonical smoke test on ~10 representative URLs.
- Generate
sitemap.xmlvia@astrojs/sitemap(or equivalent) so it derives fromsiteautomatically — never hand-write it. - Keep the old domain’s 301s live for at least 180 days (a full year is the safer default per Google’s current guidance) to give Google time to consolidate.
Related
External references: Google: Change of Address tool and Google: Site moves with URL changes.
Tags: #Hosting #Debug #Troubleshooting #SEO