You launched a site from an Astro, Next.js, or Hugo starter months ago. It works. Then someone shares an article in Slack and the unfurl shows the right title but the URL preview reads example.com. Or your RSS feed loads fine, yet every item link points at the literal placeholder https://your-domain.com/article. The cause is almost always the same: the starter shipped with a placeholder base URL (example.com, localhost, your-domain.com) and one or more templates still read from it instead of your real domain.
Fastest fix (Astro): set site in astro.config.mjs to your real https:// URL, make your RSS endpoint use site: context.site (not a hardcoded string), rebuild, then re-scrape the URL in the Facebook Sharing Debugger and LinkedIn Post Inspector. For Next.js/Hugo the equivalent is your NEXT_PUBLIC_SITE_URL / baseURL value plus any hardcoded meta templates.
This bug is silent because most page views never touch RSS or OG tags. But aggregators, RSS readers, social unfurls, and AI crawlers all parse them, so wrong links propagate to exactly the surfaces you cannot see in a normal browser visit.
Which bucket are you in
| Symptom | Most likely source | Fix section |
|---|---|---|
Slack/Discord unfurl shows example.com URL | og:url meta template hardcodes placeholder | Step 2 |
RSS item links go to your-domain.com/... | RSS endpoint site hardcoded, not context.site | Step 2 |
| Sitemap is correct, RSS/OG are not | Multiple sources of truth in the codebase | Step 1 + Step 2 |
| Works locally, broken in production only | SITE_URL env var unset in prod, fallback fires | Step 3 |
| OG image fails to load on socials | OG image URL built from placeholder domain | Step 2 |
| Fixed everything, preview still wrong | Third-party cache not yet refreshed | Step 6 |
Common causes (highest hit rate first)
1. Starter base URL never updated
// astro.config.mjs (Astro starter default)
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://example.com', // ← never updated
});
In Astro, this single site value feeds the sitemap, canonical URLs, and (when you wire it correctly) the RSS feed. If it still says example.com, everything downstream is wrong.
Spot it:
grep -rn "example\.com\|your-domain\|localhost:[0-9]" src/ astro.config.mjs
Any match outside a comment or a test fixture likely reaches production.
2. OG meta or RSS template hardcodes the placeholder
<meta property="og:url" content={`https://example.com${article.slug}`} />
// src/pages/rss.xml.js — the classic mistake
return rss({
site: 'https://your-domain.com', // ← hardcoded placeholder
// ...
});
The template author meant for you to replace these, but they shipped as-is. The fix is not just to swap the string; it is to stop hardcoding it (see Step 2 below).
Spot it: View source on any page and search for og:url — it must show your real domain. Then curl your RSS feed and check the <link> elements.
3. SITE_URL env var missing in production
const url = process.env.SITE_URL || 'https://example.com';
If SITE_URL is set locally (in .env) but never added to the production environment, the fallback fires only in prod. This is the “works on my machine” variant.
Spot it: Open your host’s dashboard (Vercel, Netlify, Cloudflare Pages) and confirm SITE_URL exists in the Production environment, not just Preview/Development.
4. Sitemap is correct but RSS / OG are not
Different files read from different constants. You updated the sitemap path months ago and never touched RSS or OG.
Spot it:
curl -s https://yourdomain.com/sitemap.xml | grep -m1 loc
curl -s https://yourdomain.com/rss.xml | grep -m1 link
If the sitemap shows your domain but RSS shows the placeholder, you have split sources of truth.
5. Static OG image URL has the placeholder
A static share image referenced as https://your-domain.com/og.png. If that host is a placeholder, Facebook and LinkedIn cannot fetch the image and the unfurl falls back to no image.
Shortest path to fix
Step 1: Find every placeholder
grep -rn "example\.com\|your-domain\|localhost:[0-9]\|<YOUR_DOMAIN>" \
src/ public/ astro.config.mjs next.config.js config.toml 2>/dev/null
List every hit. Each one needs to be updated or, better, rewired to read from a single source.
Step 2: Replace with one canonical source, referenced everywhere
Do not blind find-and-replace. Fix each context so it derives from one value:
-
Astro config (
astro.config.mjs): setsite: 'https://yourdomain.com'(no trailing slash). This is the single source of truth. -
RSS endpoint (
src/pages/rss.xml.js): pull the base URL from the endpoint context instead of hardcoding it. Per the Astro RSS docs,context.sitereturns the value you set inastro.config.mjs:import rss from '@astrojs/rss'; export function GET(context) \{ return rss(\{ title: 'My Site', description: 'My feed', site: context.site, // ← reads astro.config.mjs `site` items: [/* ... */], \}); \} -
OG / canonical meta: build absolute URLs from
Astro.site(the configured value), e.g.new URL(Astro.url.pathname, Astro.site).href. Do not interpolate a literal domain string. -
Next.js: reference
process.env.NEXT_PUBLIC_SITE_URLinmetadataBaseand youralternates.canonical/openGraph.url, and set that env var in the host.
After this step, no template should contain a hardcoded protocol-plus-domain string.
Step 3: Set the env var in production
# Vercel CLI
vercel env add SITE_URL production
# (paste: https://yourdomain.com)
# Netlify CLI
netlify env:set SITE_URL https://yourdomain.com --context production
Or set it in the host dashboard: Vercel → Project → Settings → Environment Variables; Netlify → Site configuration → Environment variables. Redeploy after adding it — env-var changes do not apply to existing deployments.
Step 4: Rebuild and verify locally
npm run build && npm run preview
# Astro preview serves on http://localhost:4321 by default
curl -s http://localhost:4321/rss.xml | grep -E "<link>|<url>" | head
curl -s http://localhost:4321/ | grep -E "og:url|rel=\"canonical\""
Every URL printed should be your real domain.
Step 5: Verify on production
curl -s https://yourdomain.com/rss.xml | grep -E "<link>" | head
curl -s https://yourdomain.com/ | grep -E "og:url|rel=\"canonical\""
If production still shows the placeholder after a successful deploy, you most likely missed Step 3 (env var) or are reading a cached deploy — hard-refresh and confirm the deploy hash changed.
Step 6: Force-refresh third-party caches
Source fixes do not retroactively update cached previews. Trigger a refresh manually:
- Facebook / Slack / WhatsApp (all read the same OG cache): paste the URL into the Facebook Sharing Debugger and click Scrape Again. CDN cache can take up to 24 hours; clicking Scrape Again two or three times usually forces it.
- LinkedIn: use the Post Inspector. LinkedIn’s cache is the stickiest — it can take up to ~7 days to fully clear even after inspection.
- RSS aggregators (Feedly and similar): they update on their next scheduled poll; there is no manual force button.
Step 7: Add a CI guard so it never regresses
# Post-build step in CI
if grep -rE "example\.com|localhost:[0-9]|your-domain" dist/; then
echo "ERROR: placeholder URL found in build output"
exit 1
fi
How to confirm it is fixed
You are done when all three of these show only your real domain:
curl -s https://yourdomain.com/rss.xml— every<link>and<guid>uses your domain.- View source of a live article —
og:urlandrel="canonical"match the page’s real URL. - The Facebook Sharing Debugger preview, after a fresh scrape, shows your domain and the correct image.
When it is not your fault
After you fix the source, third-party previews can stay wrong for hours (Facebook, up to 24h) or days (LinkedIn, up to ~7 days) because of their caches. Once you have re-scraped in the debuggers and curl confirms your origin is correct, the remaining lag is on their side, not yours.
Easy to misdiagnose as
The trap is assuming “no one reads RSS anymore,” so the wrong feed is harmless. In practice RSS and OG tags feed Slack and Discord unfurls, RSS readers, podcast/newsletter pipelines, and AI assistants that crawl feeds. A placeholder there means wrong links propagate to every surface you cannot see from a normal page visit.
Prevention
- In Astro, treat
astro.config.mjssiteas the only base URL and read it viacontext.site/Astro.siteeverywhere. Never hardcode a second copy. - Make
SITE_URL(orNEXT_PUBLIC_SITE_URL) a required env var with no default fallback so a missing value fails the build instead of silently shippingexample.com. - Keep the Step 7 CI grep permanently; it catches the regression at build time.
- Audit any new starter template for
example.comimmediately after cloning, before your first deploy.
FAQ
- Do search engines penalize a wrong RSS feed? Not as a ranking penalty, but it breaks discovery and external link surfaces, and mismatched canonical/OG URLs can cause duplicate-URL confusion for crawlers.
- Should canonical, OG, and RSS URLs be absolute? Yes. Always use absolute
https://URLs in canonical, sitemap, RSS<link>,og:url, and JSON-LD. Relative URLs are unreliable across feed readers and scrapers. - Why does Slack still show the old URL after I fixed it? Slack reads Facebook’s OG cache. Re-scrape the URL in the Facebook Sharing Debugger; Slack picks up the refreshed value shortly after.
- My sitemap is right but RSS is wrong — why? Your codebase has two sources of truth. The sitemap reads
astro.config.mjssite, but your RSS endpoint hardcodes a string. Switch the RSS endpoint tosite: context.site. - Is
example.comactually safe to use as a placeholder anywhere? Yes —example.comis reserved by RFC 2606 specifically for documentation and examples, which is why starters use it. That is also why it is so easy to forget: it never errors, it just resolves to the wrong place.