RSS / OG URL 仍显示 example.com(占位域名)

RSS 条目和 og:url 还指向 starter 模板的占位域名(example.com 或 your-domain.com)。本文说明它会从哪几处泄漏,并一次性修好每个位置。

几个月前你用 Astro、Next.js 或 Hugo 的 starter 模板上线了一个站,它一直正常。直到有人在 Slack 里分享一篇文章,展开预览标题是对的,但 URL 预览却写着 example.com。或者 RSS 订阅正常加载,可每条的链接都指向字面占位 https://your-domain.com/article。原因几乎都一样:starter 模板自带了占位基础 URL(example.comlocalhostyour-domain.com),而某些模板至今仍在读这个占位,而不是你的真实域名。

最快修复(Astro):astro.config.mjs 里把 site 设成你真实的 https:// 域名;让 RSS 端点用 site: context.site(而不是写死的字符串);重新构建;然后在 Facebook Sharing DebuggerLinkedIn Post Inspector 里重新抓取该 URL。Next.js / Hugo 对应的是 NEXT_PUBLIC_SITE_URL / baseURL,外加任何写死的 meta 模板。

这个问题之所以隐蔽,是因为绝大多数页面访问根本不会碰到 RSS 或 OG 标签。但聚合器、RSS 阅读器、社交展开预览和 AI 爬虫都会解析它们,于是错误链接恰好扩散到你在普通浏览中看不到的那些表面。

你属于哪一类

症状最可能来源修复步骤
Slack/Discord 展开显示 example.com URLog:url meta 模板写死了占位第 2 步
RSS 条目链接指向 your-domain.com/...RSS 端点 site 写死,没用 context.site第 2 步
sitemap 对了,但 RSS/OG 没对代码库里有多个可信源第 1 步 + 第 2 步
本地正常,只有生产坏生产没设 SITE_URL,fallback 触发第 3 步
社交平台拉不到 OG 图OG 图 URL 用占位域名拼出来的第 2 步
全改完了预览还是错第三方缓存还没刷新第 6 步

常见原因(按命中率从高到低)

1. starter 的基础 URL 从未更新

// astro.config.mjs(Astro starter 默认)
import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://example.com', // ← 从未更新
});

在 Astro 里,这一个 site 值会喂给 sitemap、canonical URL,以及(当你正确接线时)RSS 订阅。如果它还写着 example.com,下游全是错的。

怎么判断:

grep -rn "example\.com\|your-domain\|localhost:[0-9]" src/ astro.config.mjs

注释和测试 fixture 之外的任何匹配,都很可能进了生产。

2. OG meta 或 RSS 模板写死了占位

<meta property="og:url" content={`https://example.com${article.slug}`} />
// src/pages/rss.xml.js —— 最经典的错误
return rss({
  site: 'https://your-domain.com', // ← 写死的占位
  // ...
});

模板作者本意是让你替换这些,但它们原样上线了。修复的关键不只是换字符串,而是从此不再写死它(见下面第 2 步)。

怎么判断: 随便一页 view source 搜 og:url,它必须显示你的真实域名。然后 curl 一下 RSS 订阅,检查 <link> 元素。

3. 生产环境缺 SITE_URL env var

const url = process.env.SITE_URL || 'https://example.com';

如果 SITE_URL 只在本地(.env)设了、从没加到生产环境,那 fallback 就只在生产触发。这就是”在我机器上没问题”的那种变体。

怎么判断: 打开托管平台控制台(Vercel、Netlify、Cloudflare Pages),确认 SITE_URL 存在于 Production 环境里,而不仅仅是 Preview/Development。

4. sitemap 对了但 RSS / OG 没对

不同文件读了不同的常量。你几个月前改了 sitemap 那处,却从没动过 RSS 或 OG。

怎么判断:

curl -s https://yourdomain.com/sitemap.xml | grep -m1 loc
curl -s https://yourdomain.com/rss.xml | grep -m1 link

如果 sitemap 显示你的域名、而 RSS 显示占位,你就有了分裂的可信源。

5. 静态 OG 图 URL 带占位

一张静态分享图被引用为 https://your-domain.com/og.png。如果这个 host 是占位,Facebook 和 LinkedIn 就拉不到图,展开预览会退化成没有图。

最短修复路径

第 1 步:找出每一处占位

grep -rn "example\.com\|your-domain\|localhost:[0-9]\|<YOUR_DOMAIN>" \
  src/ public/ astro.config.mjs next.config.js config.toml 2>/dev/null

列出每个命中。每一个都要更新,或者更好——改成从单一可信源读取。

第 2 步:替换成一个 canonical 源,处处引用它

不要无脑 find/replace。把每个上下文都改成从同一个值推导:

  • Astro 配置astro.config.mjs):设 site: 'https://yourdomain.com'(末尾不带斜杠)。这就是单一可信源。

  • RSS 端点src/pages/rss.xml.js):从端点 context 取基础 URL,而不是写死。按 Astro RSS 文档context.site 返回的就是你在 astro.config.mjs 里设的值:

    import rss from '@astrojs/rss';
    
    export function GET(context) \{
      return rss(\{
        title: 'My Site',
        description: 'My feed',
        site: context.site, // ← 读取 astro.config.mjs 的 `site`
        items: [/* ... */],
      \});
    \}
  • OG / canonical meta:用配置里的值(Astro.site)拼绝对 URL,例如 new URL(Astro.url.pathname, Astro.site).href。不要插入写死的域名字符串。

  • Next.js:在 metadataBase 以及 alternates.canonical / openGraph.url 里引用 process.env.NEXT_PUBLIC_SITE_URL,并在托管平台上设好这个 env var。

这一步做完后,任何模板里都不应再有写死的”协议加域名”字符串。

第 3 步:在生产设置 env var

# Vercel CLI
vercel env add SITE_URL production
# (粘贴:https://yourdomain.com)

# Netlify CLI
netlify env:set SITE_URL https://yourdomain.com --context production

或在控制台里设:Vercel → Project → Settings → Environment Variables;Netlify → Site configuration → Environment variables。加完后要重新部署——env var 改动不会作用于已有的部署。

第 4 步:重新构建并本地验证

npm run build && npm run preview
# Astro preview 默认在 http://localhost:4321
curl -s http://localhost:4321/rss.xml | grep -E "<link>|<url>" | head
curl -s http://localhost:4321/ | grep -E "og:url|rel=\"canonical\""

打印出来的每个 URL 都应是你的真实域名。

第 5 步:生产验证

curl -s https://yourdomain.com/rss.xml | grep -E "<link>" | head
curl -s https://yourdomain.com/ | grep -E "og:url|rel=\"canonical\""

如果部署成功后生产仍显示占位,多半是漏了第 3 步(env var),或者你看的是缓存的旧部署——强制刷新并确认部署 hash 变了。

第 6 步:强制刷新第三方缓存

修源头不会回溯更新已缓存的预览。要手动触发刷新:

  • Facebook / Slack / WhatsApp(都读同一份 OG 缓存):把 URL 粘进 Facebook Sharing Debugger,点 Scrape Again。CDN 缓存最长可能要 24 小时;连点两三次 Scrape Again 通常能强制刷新。
  • LinkedIn:用 Post Inspector。LinkedIn 的缓存最顽固——即使检查过,也可能要最多约 7 天才完全清掉。
  • RSS 聚合器(Feedly 等):它们在下次定时拉取时更新,没有手动强制刷新的按钮。

第 7 步:加一道 CI 守卫,防止回归

# CI 中的构建后步骤
if grep -rE "example\.com|localhost:[0-9]|your-domain" dist/; then
  echo "ERROR: placeholder URL found in build output"
  exit 1
fi

怎么确认修好了

当以下三处都只显示你的真实域名时,就算修好了:

  1. curl -s https://yourdomain.com/rss.xml——每个 <link><guid> 都用你的域名。
  2. view source 看一篇线上文章——og:urlrel="canonical" 都和页面真实 URL 一致。
  3. Facebook Sharing Debugger 重新抓取后的预览,显示你的域名和正确的图。

哪些情况不是你操作错了

修完源头后,第三方预览可能还会错几个小时(Facebook,最长 24 小时)或几天(LinkedIn,最长约 7 天),这是它们各自缓存导致的。只要你在两个 debugger 里都重新抓取过、并且 curl 确认你的源站已正确,剩下的延迟就在它们那边,不在你这边。

容易误判的情况

最大的陷阱是以为”现在没人读 RSS 了”,所以错的订阅无所谓。实际上 RSS 和 OG 标签喂给 Slack 和 Discord 展开、RSS 阅读器、播客/邮件简报流水线,以及爬取订阅的 AI 助手。占位错误意味着错误链接扩散到你从普通页面访问里完全看不到的每一个表面。

预防建议

  • 在 Astro 里,把 astro.config.mjssite 当作唯一的基础 URL,处处通过 context.site / Astro.site 读取,绝不写第二份副本。
  • SITE_URL(或 NEXT_PUBLIC_SITE_URL)设为必填 env var、不给默认 fallback,这样值缺失时构建就直接失败,而不是悄悄上线 example.com
  • 永久保留第 7 步的 CI grep,它能在构建时就拦下回归。
  • 任何新的 starter 模板,克隆后第一次部署前就立刻审一遍 example.com

FAQ

  • 搜索引擎会因为 RSS 错误而惩罚吗? 不会作为排名惩罚,但它会破坏发现流和外链表面,而且 canonical/OG URL 不一致会让爬虫产生重复 URL 的困惑。
  • canonical、OG、RSS 的 URL 都要用绝对地址吗? 是。canonical、sitemap、RSS <link>og:url、JSON-LD 一律用绝对 https:// URL。相对地址在订阅阅读器和爬虫里并不可靠。
  • 我已经修了,为什么 Slack 还显示旧 URL? Slack 读的是 Facebook 的 OG 缓存。在 Facebook Sharing Debugger 里重新抓取该 URL,Slack 随后就会拿到刷新后的值。
  • 我的 sitemap 是对的、RSS 却是错的,为什么? 你的代码库有两个可信源。sitemap 读的是 astro.config.mjssite,而 RSS 端点写死了字符串。把 RSS 端点改成 site: context.site
  • example.com 真的可以随便当占位用吗? 可以——example.com 由 RFC 2606 专门保留给文档和示例使用,这正是 starter 喜欢用它的原因。也正因如此它特别容易被忘掉:它从不报错,只是解析到错误的地方。

相关阅读

标签: #域名 #DNS #SSL #排查 #占位 URL