Every static-site generator has a single line in its config that sets the site’s canonical URL. In Astro it is site: in astro.config.mjs. In Next.js it is NEXT_PUBLIC_SITE_URL. In Hugo it is baseURL. That one line propagates into every generated <link rel="canonical">, every <meta property="og:url">, every entry in your sitemap.xml, every JSON-LD @id, and every relative internal link that gets rewritten to absolute. When it is wrong, every one of those bleeds SEO in silence — because the pages still render, the site still deploys, and there is no build error to catch. This post is the incident write-up for exactly that failure mode on this site (cosmoswebtech.com.au) — how a one-letter typo propagated to 108 files, how the audit surfaced it, and the pre-launch checklist that would have prevented it.

Overview infographic showing how a single missing letter in an Astro site config spreads into broken canonical tags, broken social sharing metadata, weakened JSON-LD schema signals, broken on-site navigation, and a slow decline in Google Search visibility across weeks

What “Canonical Domain Drift” Actually Means

The literal typo was cosmowebtech.com.au (missing the s in cosmos) instead of the correct cosmoswebtech.com.au. The wrong domain was configured in the Astro site’s site: field at some point during setup, and everything downstream inherited it. Concretely:

  • sitemap.xml listed every URL under the wrong host, so Google Search Console kept trying to crawl a domain we do not own.
  • <link rel="canonical"> on every rendered page pointed at the wrong domain, telling Google “the real version of this page lives over there.”
  • <meta property="og:url"> in the head of every page told Facebook and LinkedIn the same wrong URL, so preview cards linked to a 404 if the crawler followed them.
  • JSON-LD (schema.org markup for Article, Organization) had @id and url fields pointing at the wrong domain, weakening every schema signal.
  • Internal links written as relative paths were being rewritten to absolute against the wrong domain during build, so every “read more” link on the site pointed users at a domain that did not resolve.

The site itself was live and working at the correct domain — Cloudflare Pages was serving cosmoswebtech.com.au fine, and users clicking through from Google would land on real pages. But every crawlable, indexable, shareable metadata field was pointing at the wrong domain. Google, Facebook, LinkedIn, and every LLM crawler that respects canonical URLs were being told the site lived somewhere else.

Why This Failure Is Silent

Diagram of the silent-failure loop: Astro builds cleanly, Cloudflare Pages deploys, Lighthouse and Web Vitals return green, and Google Analytics direct traffic looks normal — while Search Console silently reports zero discovered URLs and social preview cards link users to a 404 domain, causing weeks of invisible SEO decline before anyone notices

The scary part of a canonical-domain typo is that nothing complains. Astro built cleanly. The site deployed cleanly. Cloudflare returned 200s on every page. Lighthouse gave good scores. Web Vitals were green. Nothing in the normal deploy pipeline treats “the domain we told the site it lives at” as something worth validating.

The user impact is invisible for weeks because:

  • Google Search Console treats a canonical mismatch as a suggestion, not an error. Pages still get indexed, just at the canonical URL — which is the wrong domain — so they never rank at the actual domain.
  • Facebook / LinkedIn preview cards silently link to the wrong URL. If someone shares your post and clicks the preview, they land on a 404 hosted by whoever registered the typo domain (or on nothing).
  • Direct traffic to the correct domain works fine, so your Analytics numbers look normal.
  • Google Search visibility slowly drops over weeks as the wrong canonicals accumulate and the real URLs get de-prioritised. By the time you notice, you have already lost the ground.

The only signal we had that something was wrong was Search Console reporting “0 discovered URLs” for the site — which we initially misdiagnosed as a sitemap configuration issue.

Finding the Blast Radius

Three-step audit workflow: 1) The code audit process starting from the typo in astro.config.mjs and running grep across the src and public directories to enumerate affected files, 2) The breakdown of the 108 affected files across MDX posts (~85), Astro layouts and components (~15), JSON configs (~5), and small scripts (~2), 3) The fix and verification pattern — search and replace, verify with a re-grep for zero output, rebuild and re-deploy, then re-submit the sitemap to Search Console

Once we identified the root cause, the audit was straightforward. Every file with the wrong domain needed to be found. grep -r across the repo revealed 108 files touching the wrong string in one form or another:

grep -r "cosmowebtech" src/ public/ astro.config.mjs \
  --include="*.astro" \
  --include="*.mdx" \
  --include="*.md" \
  --include="*.json" \
  --include="*.mjs" \
  --include="*.ts" \
  | wc -l

The 108 hits broke down roughly as:

  • 1 file — the site: field in astro.config.mjs (the root cause).
  • ~85 files — MDX blog posts that had absolute URLs embedded in frontmatter (image:, ogImage:, heroImage:) or in body content (inline links to sister posts). These were written against whatever site: was configured at the time.
  • ~15 files.astro layouts and components with hardcoded absolute URLs (schema.org markup, canonical link tags, structured-data @id fields).
  • ~5 files — JSON files (sitemap-index.json, robots-adjacent configs, llms.txt references).
  • ~2 files — Small scripts and utilities that generated absolute URLs from the site domain.

The fix was one large search-and-replace across the codebase, followed by:

# Verify zero occurrences remain
grep -r "cosmowebtech" . --exclude-dir=node_modules --exclude-dir=.git
# (no output means all fixed)

Then rebuild, re-deploy, and submit a fresh sitemap.xml to Search Console.

The Pre-Launch Checklist That Would Have Prevented It

Two-step pre-launch checklist: 1) Verify the site config in astro.config.mjs resolves to a domain you actually own — grep the site line then curl the URL and confirm HTTP/2 200 before deploying, 2) After the build, spot-check three surfaces — the canonical tag on the homepage, the og:url on a random blog post, and the first URL in the sitemap — all three must return the actual production domain or you have canonical drift

Every canonical-domain drift incident I have seen (this is not the first) traces back to the same class of mistake: the site: value was set once during initial setup, before the actual domain was finalised, and never re-verified. The fix is a two-item pre-launch checklist:

1. Verify the site: config resolves to a domain you actually own.

Before the first production deploy, run:

# For Astro
grep "^  site:" astro.config.mjs

# Then check the returned host actually resolves and returns 200
curl -sI https://cosmoswebtech.com.au/ | head -1
# Expected: HTTP/2 200

If the config value returns anything other than a 200 from a domain your organisation owns, stop. Do not deploy.

2. Cross-check that generated metadata matches the config.

After the build, spot-check three surfaces:

  • The canonical tag on the homepage: curl -s https://your-domain/ | grep 'rel="canonical"'
  • The OG URL on a random blog post: curl -s https://your-domain/blog/post-slug/ | grep 'og:url'
  • The first URL in the sitemap: curl -s https://your-domain/sitemap.xml | grep -m1 '<loc>'

All three should return the actual production domain. If any of them does not, you have canonical drift.

What to Run Right Now on Your Own Site

Three-command one-minute audit for any static site (Astro, Next.js, Hugo, 11ty, Gatsby, Nuxt, Sveltekit): 1) Grep your build config for the site or baseUrl variable, 2) Curl your deployed homepage and extract the rel=canonical URL from the HTML head, 3) Fetch your deployed sitemap.xml and extract the first URL entries — then verify all three outputs match; any mismatch means you have canonical drift

If you run any static site (Astro, Next.js, Hugo, 11ty, Gatsby, Nuxt, Sveltekit) and have not deliberately verified this since the site launched, run these three commands. They take under a minute and will surface the exact class of bug this post is about:

# 1. What does your build config say the site is?
grep -E "^(site|siteUrl|baseUrl|NEXT_PUBLIC_SITE_URL)" astro.config.mjs next.config.js hugo.toml 2>/dev/null

# 2. What does the deployed site's canonical actually say?
curl -sI https://your-real-domain.com/ | head -1
curl -s https://your-real-domain.com/ | grep -oE 'rel="canonical" href="[^"]+"'

# 3. What does the deployed sitemap actually say?
curl -s https://your-real-domain.com/sitemap.xml 2>/dev/null | grep -oE '<loc>[^<]+' | head -3

If the domain in output 1 does not match the domain in output 2 and 3, you have the same problem we did.

Post-Incident: How We Re-Established Search Visibility

Post-incident recovery playbook: 1) Fix and confirm re-deploy — curl pages to verify canonicals now show the right domain, 2) Re-submit the sitemap in Google Search Console — remove the old broken sitemap, add the corrected one, 3) Request re-indexing of key pages via the URL Inspection tool on the top 10-20 pages, 4) Wait — Google's index takes 1-4 weeks to re-establish corrected canonicals as the source of truth, 5) Monitor the GSC Pages report until the Duplicate Google chose different canonical error count reaches zero. Real-world results: Search Console started reporting real Discovered URLs within 5 days of the fix; full ranking recovery took about 3 weeks; incident cost was 4-6 weeks of lost visibility and 3-4 hours of fix and audit time — all preventable with 30 seconds of pre-launch cross-check

Fixing the domain in the code was the easy part. Rebuilding the SEO position took longer. The recovery steps that mattered:

  1. Push the fix and confirm re-deploy. Deploy the corrected site. Curl a few pages to verify canonicals now show the right domain.
  2. Re-submit the sitemap in Google Search Console. Under Sitemaps, remove the old (broken) sitemap URL if it was submitted incorrectly, then add the correct one.
  3. Request re-indexing of key pages. Use the URL Inspection tool for your top 10-20 pages and click “Request Indexing” on each. Google will re-crawl within days.
  4. Wait. Google’s index takes 1-4 weeks to re-establish the corrected canonicals as the source of truth. There is no shortcut.
  5. Monitor “Pages” in Search Console. Watch for the “Duplicate, Google chose different canonical” errors that would indicate the wrong canonical is still winning somewhere. Zero of those means the fix is fully absorbed.

On our site, Search Console started reporting real “Discovered URLs” within 5 days of the fix and re-submission. Real ranking recovery took about three weeks. Cost of the incident: an estimated 4-6 weeks of lost Search visibility while the wrong canonicals accumulated, plus about 3-4 hours of fix and audit time. Preventable with 30 seconds of pre-launch cross-check.

The Cheapest SEO Insurance You Can Buy

Add this to your CI pipeline and never think about it again:

# .github/workflows/deploy.yml or your CI config
- name: Verify canonical domain matches expected
  run: |
    EXPECTED="cosmoswebtech.com.au"
    ACTUAL=$(grep "^  site:" astro.config.mjs | grep -oE 'https://[^"]+' | sed 's|https://||;s|/$||')
    if [ "$EXPECTED" != "$ACTUAL" ]; then
      echo "ERROR: site config ($ACTUAL) does not match expected domain ($EXPECTED)"
      exit 1
    fi

Ten lines of YAML. Prevents an entire class of silent SEO regression forever. Every static-site build should have some version of this.

If you run a Sydney small business website and your Google Search visibility has drifted downward inexplicably over the past few months, this is the specific thing worth checking first. The typo does not have to be visible to a human — a missing letter in a config file three months ago is enough to explain a slow bleed of ranking that no other diagnostic will surface.