How a single config line can make your production site invisible to Google — and the principled fix that prevents it from ever happening again.
The Discovery
We found the bug during a routine SEO audit. Every page on our production site — every canonical URL, every og:url meta tag, every entry in the sitemap, every Sitemap: directive in robots.txt — was telling Google that the authoritative version of our content lived at mintdeals2026.pages.dev, a Cloudflare Pages preview domain.
Our production domain? Google had effectively been told to ignore it.
The damage was invisible from the browser. Users could visit the site, browse products, place orders — everything worked. But behind the scenes, we were hemorrhaging search visibility. Google was either indexing the wrong domain or, worse, treating both as duplicates and choosing the .pages.dev URL as canonical.
How It Happened
The root cause was a single line in astro.config.mjs:
export default defineConfig({
site: 'https://mintdeals2026.pages.dev',
// ...
});
This happened during a routine deployment change. Someone updated the site property to match the Cloudflare Pages domain where the build was being tested. The commit message read “chore: update site base URL to mintdeals2026.pages.dev” — a mundane, unremarkable change that sailed through code review.
But in Astro, the site config feeds import.meta.env.SITE — the value that downstream code uses to build every absolute URL the site emits. And to make things worse, the Layout component didn’t even use import.meta.env.SITE. It had its own hardcoded constant:
// Layout.astro
const SITE_URL = 'https://mintdeals2026.pages.dev';
const canonicalURL = new URL(Astro.url.pathname, SITE_URL).href;
So did the sitemap and robots.txt, but with a fallback pattern that looked correct:
// sitemap.xml.ts
const SITE = import.meta.env.SITE || 'https://mintdeals2026.pages.dev';
The code was saying: “use the config value, but if it’s missing, fall back to the preview domain.” A reasonable defensive pattern — except the fallback should never have been a non-production URL.
Why This Class of Bug Is So Dangerous
This bug has three properties that make it especially insidious:
1. Zero Runtime Symptoms
The site renders perfectly. No errors in the console. No broken pages. No user complaints. The damage is entirely in the HTML <head> — the part humans never look at but crawlers read first.
2. Delayed Impact
SEO damage is not instant. Google discovers the wrong canonical over days or weeks of crawling. By the time you notice a drop in organic traffic, the misconfigured URLs have been indexed for a while, and recovery takes equally long.
3. It Passes Code Review
A change to a site URL looks routine. Reviewers scan for logic errors, security flaws, broken imports — not whether a URL string is pointing to staging or production. The commit message said “update site base URL,” and that’s exactly what it did. The review question nobody asked was: update it to what?
The Principled Fix
The fix itself is trivial — change a few strings. But the principled fix is about establishing a pattern that makes this category of mistake structurally impossible.
Principle 1: The Production Domain Is Configuration, Not Code
Production URLs should never appear as string literals in source files. They belong in exactly one place: the build/deploy configuration, injected via environment.
// astro.config.mjs — the SINGLE source of truth
export default defineConfig({
site: 'https://mintdeals.com',
});
Every other file in the codebase should read from import.meta.env.SITE or Astro.site. No local constants. No fallbacks. If the value is missing, the build should fail loudly, not silently default to the wrong domain.
Principle 2: Never Fallback to a Non-Production Domain
This pattern is a trap:
// DANGEROUS — the fallback masks a misconfiguration
const SITE = import.meta.env.SITE || 'https://staging.example.com';
If import.meta.env.SITE is undefined, you don’t want a graceful fallback — you want an immediate, obvious failure. A build that crashes because SITE is missing is infinitely better than a build that silently ships staging URLs to production.
// SAFE — fail fast, fail loud
const SITE = import.meta.env.SITE;
if (!SITE) throw new Error('SITE is not configured');
Or simply rely on the framework. Astro provides Astro.site for exactly this purpose.
Principle 3: One Declaration, Many References
The bug was amplified because the URL was declared independently in five separate files. Each one was a potential point of drift. The fix isn’t just correcting the value — it’s eliminating the redundancy:
| Before (5 declarations) | After (1 declaration, 4 references) |
|---|---|
astro.config.mjs: hardcoded | astro.config.mjs: single source of truth |
Layout.astro: own SITE_URL const | Layout.astro: reads Astro.site |
[locale].astro: own SITE_URL const | [locale].astro: reads Astro.site |
sitemap.xml.ts: env with fallback | sitemap.xml.ts: reads import.meta.env.SITE |
robots.txt.ts: env with fallback | robots.txt.ts: reads import.meta.env.SITE |
Principle 4: Make It Testable
Add a build-time or CI check that grep-tests the output for non-production domains:
# In CI pipeline — after build
if grep -r "pages.dev" dist/ --include="*.html" --include="*.xml" -l; then
echo "ERROR: Build output contains non-production URLs"
exit 1
fi
This is a 3-line addition to your CI pipeline that would have caught this bug before it ever shipped. The check is cheap, deterministic, and catches the entire category of problem regardless of which file introduces it.
Principle 5: Audit the <head>, Not Just the <body>
Most QA processes focus on what users see — the rendered page, the interactions, the layout. But search engines care enormously about what’s in <head>:
<link rel="canonical"><meta property="og:url"><script type="application/ld+json">(structured data)/sitemap.xml/robots.txt
These are first-class outputs of your application. Treat them as testable contracts:
// In your e2e test suite
test('canonical URL points to production domain', async ({ page }) => {
await page.goto('/');
const canonical = await page.$eval(
'link[rel="canonical"]',
el => el.getAttribute('href')
);
expect(canonical).toMatch(/^https://mintdeals.com/);
});
The Broader Lesson
This incident is a specific case of a general anti-pattern: environment-specific values hardcoded in source code. The same class of bug manifests as:
- API endpoints pointing to staging
- Analytics tracking IDs for the dev property
- OAuth redirect URIs pointing to localhost
- CDN URLs pointing to a test bucket
- Payment processor keys set to sandbox mode in production
The principle is always the same: environment-specific values must come from the environment, not the source code. The source code defines how to use a URL; the environment defines which URL to use.
Checklist: Preventing Domain Misconfiguration
Use this as a code review checklist for any PR that touches URLs, domains, or site configuration:
- Is the production domain declared in exactly one place?
- Do all other references read from config/environment, not hardcoded strings?
- Are there any fallback values that could silently mask a misconfiguration?
- Does CI validate that build output contains no staging/preview domain references?
- Do e2e tests verify canonical URLs, og:url, sitemap, and robots.txt?
- Is the
site/BASE_URL/ equivalent config property set correctly for each deployment target?
Recovery
If you’ve already shipped wrong canonical URLs, here’s the recovery path:
- Fix and deploy immediately. Every hour the wrong canonicals are live is more crawl budget wasted on the wrong domain.
- Resubmit sitemap in Google Search Console. Go to Search Console > Sitemaps > submit the corrected sitemap URL. This prompts Google to re-crawl.
- Request indexing of key pages. Use the URL Inspection tool in Search Console to request re-indexing of your most important pages.
- Monitor the “Pages” report. Watch for the old
.pages.devURLs to drop out of the index over the following 2–4 weeks. - Check for backlinks. If anyone linked to the
.pages.devdomain, you’ll want 301 redirects from the preview domain to production to preserve link equity.
Conclusion
A one-line config change made our production site invisible to Google. No alerts fired. No tests failed. No users complained. The damage accumulated silently over weeks.
The fix was five minutes of editing. The real fix — the one that prevents it from ever happening again — is structural: single source of truth for the domain, no fallback values that mask misconfigurations, CI checks on build output, and e2e tests that treat the <head> as a first-class contract.
Your site’s SEO metadata is not boilerplate. It’s infrastructure. Treat it accordingly.
This post was written based on a real incident (MT-124) in which canonical URLs, og:url, sitemap entries, and robots.txt all pointed to a Cloudflare Pages preview domain instead of the production domain for approximately three weeks before detection.