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/...">. Search Console will keep indexing the old domain as primary and search traffic will never fully move. The bug is almost never a single root cause; it’s a stack of hard-coded URLs + CDN cache + Google not having re-crawled yet. This article peels them off layer by layer.
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.
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. robots.txt referencing an absolute sitemap URL on the old domain has the same problem.
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 → newsite.com 301s are in place, but pages on the new domain still canonical back to oldsite.com — Google sees a 301 → canonical → 301 loop and refuses to index either.
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 primary property not changed
The GSC property is still https://oldsite.com, with no https://newsite.com property added and no “Change of address” submitted. Google can crawl the new domain but doesn’t know it’s the successor to the old one — migration signals don’t transfer.
How to spot it: Log into Search Console, check both properties exist, and verify Settings → Change of address has been submitted.
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}`} />
Set SITE_URL=https://newsite.com on the host (Vercel / Netlify / Cloudflare) for both preview and production.
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/....
Step 3: Purge CDN / host cache
- Cloudflare: dashboard → Caching → Configuration → Purge Everything
- Vercel: Deployments → latest → Redeploy (do not reuse build cache); Edge cache invalidates on new deploy
- Cloudflare Pages: Settings → Builds & deployments → Purge cache
- Netlify: Deploys → Clear cache and deploy site
Then curl -I should show cf-cache-status: MISS or EXPIRED, 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:
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. - In the old property
https://oldsite.com, go to Settings → Change of address and walk the wizard, selecting the new property. - For the top 20 URLs, run URL Inspection → “Request indexing” to force a re-crawl.
- Submit the new sitemap:
https://newsite.com/sitemap.xml.
Google takes 4-8 weeks to fully transfer migration signals. The old domain’s 301s must stay live the whole time.
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 picks upsiteautomatically — never hand-write - Keep the old domain’s 301s live for at least 6 months to give Google time to consolidate signals
Related
Tags: #Hosting #Debug #Troubleshooting #SEO