site:vercel.app your-project 一搜,发现一堆 preview URL 被 Google 收录了:your-site-git-feature-x.vercel.app、your-site-abc123.vercel.app,还有一些早忘了的部署哈希。更糟的是某些 preview URL 在品牌词上反而排在你正式域名前面。原因是 Vercel、Netlify、Cloudflare Pages 上的 preview host 默认就是公开的。如果代码里没识别 preview 主机名并发出 noindex 头或 robots 屏蔽,Googlebot 会跟着任何地方的链接 —— Slack 分享、PR 评论、Notion 文档、推文里的过期链接 —— 把 preview 当真页爬。重复内容惩罚、品牌词被分流随之而来。
常见原因
按”哪条路径泄漏”排序。
1. preview 主机名上没有 X-Robots-Tag: noindex
Vercel 和 Netlify 默认不会给 preview 部署加 noindex。它们就是真实可爬页面。如果你代码里不检测 preview 主机名并主动发 header,Google 就会收录。
如何识别:curl -I https://your-site-abc123.vercel.app/ 没有 x-robots-tag: noindex。
2. preview URL 在公开 Slack/Discord 里被 unfurl
Slack 和 Discord 都会在贴出链接时去抓预览。这一抓如果链接后来出现在任何公开归档里(Discourse 论坛、公开 Slack 导出、GitHub issue),就足以把它推进 Google 爬队列。
如何识别:被收录的 preview URL 能追到 Slack/Discord 的分享记录。site:vercel.app 加上频道归档名能找到原始链接。
3. 公开 GitHub 仓库里的 PR 评论暴露 preview 链接
Vercel/Netlify 的 GitHub 集成会自动在 PR 上贴 preview URL 评论。公开仓库这些评论对全世界可见。Google 爬公开 GitHub,跟着链接进去把 preview 收了。
如何识别:被收录的 preview 与公开仓库的 PR 一一对应,按 PR 号顺序排列。
4. 生产代码错把 preview URL 写进 sitemap
sitemap 生成脚本用了 process.env.VERCEL_URL 而不是写死正式域名,于是 preview 部署的 sitemap 指向自己。
如何识别:curl https://your-site-abc123.vercel.app/sitemap.xml 里 URL 开头都是 https://your-site-abc123.vercel.app/,不是正式域名。
5. preview 部署没开密码保护
Vercel “Deployment Protection”(以前叫 Password Protection)可以把 preview 用密码挡起来。Hobby / 免费档默认关闭,Pro 也要显式打开。
如何识别:开一个隐身窗口贴一个 preview URL —— 直接打开、没有 auth 提示。
6. canonical 指向 preview 主机而不是正式域名
<link rel="canonical" href="${import.meta.env.SITE}/path"> 其中 SITE 按部署变化的话,preview 部署的 canonical 会带 preview 域名。Google 读 canonical 把 preview 当作 canonical。
如何识别:在 preview 部署上看源码,<link rel="canonical"> 的 href 是 your-site-abc123.vercel.app/...。
7. 旧 preview URL 仍可访问且有外链
Vercel 默认永久保留部署 URL(“feature”,做永久链)。几个月前公开贴出的 preview URL 现在还在 Google 爬队列里、定期回爬。
如何识别:被收录的 preview URL 里有几个月前的部署。即使今天修了配置,老的还会持续一段时间。
开始排查前
site:vercel.app your-project和site:netlify.app your-project(或你的 provider 后缀)搜一下,记下数量。- Search Console 里看有没有 “Duplicate without user-selected canonical” 或 “Crawled - currently not indexed” 警告。
- 确认你有 provider 后台 “Deployment Protection” 的权限。
- 确认 preview 构建时
process.env.VERCEL_URL或process.env.URL解析成什么(每次部署都变)。 - 确认仓库是公开还是私有,这会显著改变泄漏面积。
需要收集的信息
- 抽 5 条被收录的 preview URL 及其对应的 deployment ID。
- 一个 preview URL 的
curl -I输出,看x-robots-tag、x-vercel-deployment-url。 - canonical URL 生成代码(grep
canonical、og:url、import.meta.env.SITE、VERCEL_URL)。 - sitemap 生成代码(常在
scripts/build-sitemap.mjs或自动生成)。 - provider 的 deployment protection 设置。
- Search Console “Pages” 报告按
vercel.app或netlify.app过滤,统计受影响 URL 数。
分步修复
按”先止血”排序。
步骤 1:给 preview 主机名发 noindex 头
Vercel,vercel.json:
{
"headers": [
{
"source": "/(.*)",
"has": [
{ "type": "host", "value": "(?!your-site\\.com$).*\\.vercel\\.app" }
],
"headers": [
{ "key": "X-Robots-Tag", "value": "noindex, nofollow" }
]
}
]
}
Netlify,netlify.toml:
[[headers]]
for = "/*"
[headers.values]
X-Robots-Tag = "noindex, nofollow"
[context.deploy-preview.environment]
ROBOTS_NOINDEX = "true"
Astro / 框架层:
---
const isPreview =
Astro.url.hostname.endsWith(".vercel.app") ||
Astro.url.hostname.endsWith(".netlify.app");
---
{isPreview && <meta name="robots" content="noindex, nofollow" />}
这一步先止血,新的爬不再进入索引。已经在索引里的需要步骤 4 主动清掉。
步骤 2:canonical 永远指向正式主机
写死或在环境里固定:
// src/lib/site.ts
export const SITE = "https://your-site.com"; // 永远不要从 VERCEL_URL 派生
<link rel="canonical" href={new URL(Astro.url.pathname, SITE).toString()} />
这样即使在 preview 部署上 canonical 也指向正式域名,Google 会把排名信号合并过去。
步骤 3:锁定 preview 部署访问
Vercel:Project → Settings → Deployment Protection → 给 preview 和 development 打开 “Vercel Authentication” 或 “Password Protection”。
Netlify:Site → Site Configuration → Visitor Access → 给 branch deploy 和 deploy preview 开 “Password protection”。
之后 preview URL 需要登录或密码,Googlebot 进不去。
步骤 4:把已被收录的 preview URL 主动提交移除
针对已经在 Google 索引里的:
- Search Console → Removals → New Request → URL prefix
https://your-site-abc123.vercel.app/。 - 提交。Removals 是临时的(6 个月),但立刻生效。
- 想永久清,再让那些 host 返回
404/410(配合步骤 1 的 noindex)。
只有这条路径能快速清除已收录 preview,等自然回爬可能要数周。长尾清理流程见 old deployment url in search。
步骤 5:审 sitemap 是否泄漏
在一次 preview 构建上跑:
npm run build
grep -E "vercel\.app|netlify\.app" dist/sitemap*.xml
有命中就说明 sitemap 生成器用了按部署变化的主机。换成写死的 SITE:
import { SITE } from "./site";
const urls = posts.map((p) => `${SITE}${p.url}`);
步骤 6:关掉公开 GitHub PR 上的 Vercel preview 评论
公开仓库:
Vercel → Project → Settings → Git → 取消 “Comments on Pull Requests” 或设为 “Off”。
这阻止 Google 通过 GitHub 抓新 preview URL。历史 PR 评论仍在 Google 记忆里,老的 URL 仍需走步骤 4。
步骤 7:preview 上提供独立的 robots.txt(兜底)
加一层保险。按 host 区分 robots.txt:
// src/pages/robots.txt.ts
export const GET = ({ request }: { request: Request }) => {
const url = new URL(request.url);
const isPreview =
url.hostname.endsWith(".vercel.app") ||
url.hostname.endsWith(".netlify.app");
const body = isPreview
? "User-agent: *\nDisallow: /\n"
: "User-agent: *\nAllow: /\nSitemap: https://your-site.com/sitemap.xml\n";
return new Response(body, { headers: { "content-type": "text/plain" } });
};
注意:步骤 1 的 X-Robots-Tag 必须保留 —— robots.txt 是建议性的,header 才是强制的。相关排查见 robots txt not working。
验证
curl -I https://your-site-abc123.vercel.app/返回x-robots-tag: noindex, nofollow。curl https://your-site-abc123.vercel.app/sitemap.xml返回 404 或里面只剩正式域名 URL。- preview 部署的源码里能看到
<meta name="robots" content="noindex,nofollow">。 - 隐身窗口打开 preview URL 提示登录(前提是已开 deployment protection)。
- Search Console “Removals” 里提交的 preview URL 标记 “Approved”。
- 大约 7 天后,
site:vercel.app your-project数量显著下降。
长期预防
- preview host 的
noindex在vercel.json/netlify.toml里写死并入仓库,不依赖运行时开关。 - canonical 始终来自单一写死的
SITE常量,不要派生自运行时环境变量。 - 所有非生产部署默认开 deployment protection。
- CI 加一道检查:对 preview URL 跑
curl -I,没有x-robots-tag直接 fail。 - Search Console 只 claim 正式域名,不要 claim
*.vercel.app(会扩大索引面)。 - 每季度搜一次
site:vercel.app your-project;一个 preview 被收录就足以分流品牌词。
常见坑
- 只在
<meta>里加noindex而不在响应头里加 —— Google 两者都认,但 header 更难误删。 - 只靠
robots.txt—— 对已知 URL Google 不一定遵守;真正能赶出索引的是 header。 - 重构时一不小心把 noindex 加到生产域名上 —— 修改完务必在生产测一次。
- 忘了 PR 合并后 preview URL 还能活几个月,因为部署 URL 不会被自动清。见 duplicate domain versions indexed。
- “为了监控”在 Search Console 加
*.vercel.app属性 —— 反而告诉 Google 多爬。
常见问答
Q: 已被收录的 preview URL 多久能从 Google 掉出来?
Search Console “Removals” 立刻生效(6 个月临时)。永久移除靠 noindex 头 + 自然回爬,每条 1-4 周不等,低流量老 URL 更久。
Q: 要不要把 preview URL 301 重定向到正式域名?
一般要做 —— 配合 noindex,把链接权重合到正式域名,再让 preview 出索引。vercel.json 里加 preview host 到正式同路径的 redirect。
Q: preview 加 noindex 会不会影响 Vercel 自己的部署检查?
不会 —— Vercel 用 HTTP 200 状态判断部署,不看 robots 指令。bot 会忽略 X-Robots-Tag。
Q: 自定义分支别名(比如 staging.your-site.com)也泄漏了,同一套修法?
是的 —— 把 host 正则扩展到所有非生产主机名。staging 按 preview 处理:noindex + deployment protection。