Canonical Still Points to Old Domain After a Move

Moved to a new domain but pages still emit the old canonical — fix hard-coded URLs, CDN cache, sitemap, 301s, and Search Console.

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 observeMost likely layerJump to
curl -s https://newsite.com/page | grep canonical shows oldsite, even on a fresh deployHard-coded site URL in sourceStep 1
Source is fixed in dist/ but production still serves old canonical; curl -I shows a cache HITCDN / host cacheStep 3
sitemap.xml still lists oldsite.com <loc> entriesSitemap / robots not updatedStep 4
oldsite.com 301s to new, but new pages canonical back to old301 ↔ canonical loopStep 4
HTML is correct everywhere, GSC still shows old domain as canonicalGoogle hasn’t re-processedStep 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 set VERCEL_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

  1. Add https://newsite.com as a property in GSC and verify it. Use a Domain property for both old and new so the Change of address tool is available.
  2. Implement the homepage 301 first — the tool runs pre-move checks and will flag a missing oldsite.com -> newsite.com 301 on the home page as a critical failure.
  3. 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.
  4. Submit the new sitemap (https://newsite.com/sitemap.xml) under the new property.
  5. 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

  1. curl -s https://newsite.com/ | grep canonical returns only https://newsite.com/... — no oldsite anywhere.
  2. curl -I https://newsite.com/ shows a fresh response (cf-cache-status: MISS/EXPIRED, or a new x-vercel-id).
  3. curl -I https://oldsite.com/deep/path returns 301 to the same deep path on the new domain.
  4. In GSC, run URL Inspection on a new-domain URL → “Google-selected canonical” matches your declared canonical.
  5. 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.xml via @astrojs/sitemap (or equivalent) so it derives from site automatically — 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.

External references: Google: Change of Address tool and Google: Site moves with URL changes.

Tags: #Hosting #Debug #Troubleshooting #SEO