site:vercel.app your-project 一搜,发现 preview URL 被 Google 收录了:your-site-git-feature-x.vercel.app、your-site-abc123.vercel.app,还有一些早忘了的部署哈希。更糟的是某些 preview URL 在品牌词上反而排在你正式域名前面。
最快的修法:先确认出问题的那个 host 是否缺了 X-Robots-Tag: noindex(curl -I <preview-url>),把这个头加回来,再修掉任何指向 preview 的 sitemap/canonical,最后在 Search Console 的 Removals 里提交这些 URL,几小时内(而不是几周)把它们清出索引。下面是细节。
有一点变了、很多人会踩坑:截至 2026 年 6 月,Vercel 和 Netlify 对标准 preview 部署都已经会自动加上 X-Robots-Tag: noindex。所以如果 preview 被收录了,你几乎不会是”忘了加 noindex”那种情况,而是落在几个具体的边缘场景里:非生产分支上的自定义域名、可被索引的最新 branch deploy、旧的生产部署,或者一份与 header 矛盾的 sitemap/canonical。下面几节会帮你对号入座。
你属于哪一类?
| 现象 | 可能原因 | 跳到 |
|---|---|---|
被收录的 host 是 *.vercel.app / *.netlify.app | header 被覆盖掉,或 sitemap/canonical 盖过了它 | 原因 1、4、6 |
被收录的 host 是自定义子域名(如 staging.your-site.com) | 非生产分支上的自定义域名 —— Vercel 在这种情况下不发 noindex | 原因 1 |
| 只有最新的 branch deploy 被收录 | Netlify 默认让最新 branch deploy 保持可索引 | 原因 1 |
| 隐身窗口打开 preview 直接进、没登录提示 | Deployment Protection 设成了 None | 原因 5 |
| 被收录的 preview 与公开仓库 PR 一一对应 | preview 链接被从公开 GitHub 抓走 | 原因 3 |
| 几个月前的旧部署仍被收录 | 旧 URL 仍可访问、还在 Google 爬队列里 | 原因 7 |
常见原因
按”哪条路径泄漏”排序。
1. 出问题的那个 host 上缺了 X-Robots-Tag: noindex
这是核心诊断点。Vercel 和 Netlify 对标准 preview 部署都会自动注入 X-Robots-Tag: noindex,所以泄漏几乎都意味着被收录的那个具体 host 上没有这个头。有三种被官方记录在案的成因:
- Vercel 把自定义域名挂在非生产分支上。 这是最常见的一种。按 Vercel 官方文档,当你把一个自定义域名(如
staging.your-site.com)分配给非生产分支时,Vercel 不会加X-Robots-Tag: noindex。自动保护只覆盖系统生成的*.vercel.appURL。 - Netlify 的最新 branch deploy。 Netlify 会给 Deploy Preview、旧的 branch deploy、未发布的生产部署加
noindex,但默认让最新的 branch deploy 保持可索引。如果你长期维护一个分支(比如staging),它的最新部署就是可爬的。 - 框架或中间件覆盖了响应头,在平台 header 生效前把平台的
noindex抹掉了。
如何识别:curl -I https://<被收录的host>/ 里没有 x-robots-tag: noindex。一定要对着 Google 收录的那个确切 host 跑,而不是新开一个 preview。
2. preview URL 在公开 Slack/Discord 里被 unfurl
Slack 和 Discord 都会在贴出链接时去抓预览。这一抓如果链接后来出现在任何公开归档里(Discourse 论坛、公开 Slack 导出、GitHub issue),就足以把它推进 Google 爬队列。unfurl 本身不会收录页面,但它制造了 Google 会跟进的入站链接。
如何识别:被收录的 preview URL 能追到 Slack/Discord 的分享记录。在 Google 搜该频道的公开网页归档能找到原始链接。
3. 公开 GitHub 仓库里的 PR 评论暴露 preview 链接
Vercel/Netlify 的 GitHub 集成会自动在 PR 上贴 preview URL 评论。公开仓库这些评论对全世界可见,Google 爬公开 GitHub、跟着链接进到 preview。这只有在 preview 本身缺 noindex(见原因 1)时才会真正导致收录 —— 但它是 Google 最常用来发现这个 URL 的入口。
如何识别:被收录的 preview 与公开仓库的 PR 一一对应,按 PR 号大致顺序排列。
4. 生产代码把 preview URL 写进 sitemap
sitemap 生成脚本用了 process.env.VERCEL_URL(或 Netlify 的 process.env.URL / process.env.DEPLOY_PRIME_URL)而不是写死正式域名,于是 preview 部署的 sitemap 指向自己。sitemap 里的条目是很强的索引信号,在 Google 眼里可能盖过 header。
如何识别:curl https://<preview-host>/sitemap.xml 里 URL 开头都是 preview host,不是正式域名。
5. preview 部署的 Deployment Protection 被关掉了
Vercel 新项目默认是 Standard Protection(prod_deployment_urls_and_all_previews),会把每个 preview 挡在 Vercel 登录后面。但如果有人把项目(或团队默认值)设成了 None,preview 就完全公开了。Netlify 的 branch deploy / deploy preview 密码保护则要你手动开才有。
如何识别:开一个隐身窗口贴一个 preview URL —— 直接打开、没有 auth 提示。(开了 Standard Protection 的话,你会看到一个 “Log in to Vercel” 的页面。)
6. canonical 指向 preview 主机而不是正式域名
<link rel="canonical" href="${import.meta.env.SITE}/path"> 其中 SITE 按部署派生的话,preview 部署的 canonical 就会带 preview 域名。Google 读到 preview 上这个自指的 canonical,即使页面有 noindex 头,也可能把 preview 当成 canonical 版本 —— 信号互相打架会扰乱合并。(严格说,一个 noindex 的页面本就不该同时是 canonical 目标;信号混乱正是 Search Console 里”Google 选择了与用户不同的 canonical”的成因。)
如何识别:在 preview 部署上看源码,<link rel="canonical"> 的 href 开头是 preview host,不是正式域名。
7. 旧的 preview/生产 URL 仍可访问且有外链
Vercel 默认永久保留部署 URL(“feature”,做永久链)。几个月前公开贴出的 URL 现在还在 Google 爬队列里、定期回爬。旧的生产 部署 URL 尤其危险,因为它当时在线时根本没有 noindex。
如何识别:被收录的 URL 里有几个月前的部署。即使今天修了配置,老的也会持续到被主动清除为止。
开始排查前
site:vercel.app your-project和site:netlify.app your-project(或你的 provider 后缀)搜一下,记下数量和确切的 host。- Search Console 里看有没有 “Duplicate without user-selected canonical”、“Duplicate, Google chose different canonical than user” 或 “Crawled - currently not indexed”。
- 判断被收录的 host 是系统生成的
*.vercel.app、自定义子域名,还是 Netlify 的 branch deploy —— 这决定了对应哪条原因。 - 确认团队/项目的 Deployment Protection 级别(Vercel:Settings → Deployment Protection)。
- 确认 preview 构建时
process.env.VERCEL_URL/process.env.URL解析成什么(每次部署都变)。 - 确认仓库是公开还是私有,这会显著改变发现面积。
需要收集的信息
- 抽 5 条被收录的 URL 及其对应的 deployment ID / host。
- 对被收录的那个确切 host 跑
curl -I的输出,看x-robots-tag、x-vercel-deployment-url(Vercel)或x-nf-request-id(Netlify)。 - 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:在出问题的 host 上恢复 noindex 头
如果 curl -I 看不到 x-robots-tag,你就是原因 1。平台的自动 header 没覆盖到你这种情况,所以要显式加上。
Vercel —— 自定义域名 preview 需要在 vercel.json 里按非生产 host 加规则。Vercel 文档建议优先用框架来发头,vercel.json 是兜底方案:
{
"headers": [
{
"source": "/(.*)",
"has": [
{ "type": "host", "value": "(?!your-site\\.com$).*" }
],
"headers": [
{ "key": "X-Robots-Tag", "value": "noindex, nofollow" }
]
}
]
}
Netlify —— 在所有非生产 context 上强制 noindex(这会覆盖 Netlify 让最新 branch deploy 保持可索引的默认行为):
[[headers]]
for = "/*"
[headers.values]
X-Robots-Tag = "noindex, nofollow"
[context.production]
[context.production.environment]
# 生产保持不带这个头 —— 只通过下面的规则区分
因为全局 [[headers]] 块对所有环境生效,要么用构建时生成的 _headers 文件按分支细分规则,要么靠 process.env.CONTEXT(production / deploy-preview / branch-deploy)来判断,确保生产保持可索引。
框架层(任意 host 通用,以 Astro 为例):
---
const host = Astro.url.hostname;
const isProd = host === "your-site.com";
---
{!isProd && <meta name="robots" content="noindex, nofollow" />}
用生产 host 做白名单(而不是用 preview host 做黑名单)更安全 —— 新出现的 preview 后缀漏不过去。
这一步先止血,新的爬不再进入索引。已经在索引里的还要靠步骤 4 主动清掉。
步骤 2:canonical 永远指向正式主机
写死或在环境里固定,让 preview 永远不会自指:
// 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 会把排名信号合并过去,不再把 preview 当成独立页。
步骤 3:锁定 preview 部署访问
Vercel:Project → Settings → Deployment Protection → 设成 Standard Protection(或 All Deployments),让 preview 需要 Vercel 登录。再确认团队级默认值(Team Settings → Deployment Protection)不是 None,否则会悄悄覆盖新项目。
Netlify:Site → Site configuration → Visitor access → 给 branch deploy 和 deploy preview 开 Password protection。
开了保护之后,Googlebot 撞上登录墙、根本爬不到 preview —— 这比 noindex 更彻底,因为 bot 连页面正文都看不到。
步骤 4:把已被收录的 URL 主动提交移除
针对已经在 Google 索引里的:
- Search Console → Removals → New Request → Remove all URLs with this prefix →
https://your-site-abc123.vercel.app/。 - 提交。几小时内生效,但是临时的、大约 6 个月(按 Google Removals 文档)。它只是隐藏 URL,不会阻止爬取。
- 想永久清,配合步骤 1 的
noindex头,让 Google 下次回爬时把 URL 丢掉,或者让那些已废弃的 host 返回404/410。
只有这条路径能快速清除已收录 preview,等自然回爬再掉出来可能每条要 1-4 周。长尾清理流程见 old deployment url in search。
步骤 5:审 sitemap 是否泄漏
在一次 preview 构建上跑:
npm run build
grep -E "vercel\.app|netlify\.app|deploy-preview" dist/sitemap*.xml
有命中就说明生成器用了按部署变化的主机。换成写死的 SITE:
import { SITE } from "./site";
const urls = posts.map((p) => `${SITE}${p.url}`);
一个不输出 sitemap(或只列正式域名 URL)的 preview,就少了一个本可盖过 noindex 的强索引信号。
步骤 6:关掉公开 GitHub PR 上的 preview 评论
公开仓库里,自动贴出的 preview 链接是主要的发现路径:
Vercel → Project → Settings → Git → 关掉 “Comments on Pull Requests”。
Netlify → Site configuration → Build & deploy → Deploy notifications → 移除 GitHub PR 评论集成。
这阻止 Google 通过 GitHub 抓新的 preview URL。历史 PR 评论仍在 Google 记忆里,老的 URL 仍需走步骤 4。
步骤 7:在 preview 上提供独立的 robots.txt
兜底一层。按 host 在非生产环境上 disallow:
// src/pages/robots.txt.ts
export const GET = ({ request }: { request: Request }) => {
const url = new URL(request.url);
const isProd = url.hostname === "your-site.com";
const body = isProd
? "User-agent: *\nAllow: /\nSitemap: https://your-site.com/sitemap.xml\n"
: "User-agent: *\nDisallow: /\n";
return new Response(body, { headers: { "content-type": "text/plain" } });
};
注意:步骤 1 的 X-Robots-Tag 头也要保留。robots.txt 只能阻止爬取,并不能把 Google 已经知道的 URL 清出索引,而且一个被 Disallow 但有外链的页面仍可能以裸 URL 形式出现在结果里。真正能去索引的是 header。相关排查见 robots txt not working。
如何确认已修好
curl -I https://<被收录的host>/返回x-robots-tag: noindex, nofollow。curl https://<preview-host>/sitemap.xml返回 404,或里面只剩正式域名 URL。- preview 部署的源码里能看到
<meta name="robots" content="noindex, nofollow">,且 canonical 指向正式域名。 - 隐身窗口打开 preview URL 提示 Vercel 登录 / 密码(前提是已开 Deployment Protection)。
- Search Console → Removals 里提交的 URL 标记 Approved。
- 大约 1-2 周后,
site:vercel.app your-project数量下降(Removals 立即隐藏,永久去索引要等下次回爬)。
长期预防
- 把非生产 host 的
noindex写进vercel.json/netlify.toml并入仓库,按生产 host 白名单来判断 —— 不依赖只在运行时存在的开关。 - canonical 始终来自单一写死的
SITE常量,绝不派生自VERCEL_URL这类运行时环境变量。 - 所有非生产部署的 Deployment Protection 保持 Standard(或更严),并锁死团队级默认值,让新项目自动继承。
- CI 加一道检查:对 preview URL 跑
curl -I,缺x-robots-tag: noindex直接 fail。 - Search Console 只 claim 正式域名,绝不加
*.vercel.app属性(那等于告诉 Google 多爬这些 host)。 - 每季度搜一次
site:vercel.app your-project;一个 preview 被收录就足以分流品牌词。
常见坑
- 以为是自己”忘了加 noindex 头”,其实 Vercel/Netlify 都会自动加 —— 真正的缺口通常是自定义域名 preview,或一份与之矛盾的 sitemap/canonical。
- 只在
<meta>里加noindex而不在响应头里加 —— Google 两者都认,但 Removals 请求 + header 才是快速赶出的组合。 - 只靠
robots.txt—— 对已知 URL,Google 不会因为Disallow就去索引;能去索引的是 header。 - 用 preview 后缀做黑名单而不是用 生产 host 做白名单 —— 新后缀会漏过去。改用生产白名单。
- 重构时把
noindex加成全局、一不小心连生产也加上了 —— 改完务必在生产再测一次。 - 忘了 PR 合并后 preview URL 还能活几个月,因为部署 URL 不会被自动清。见 duplicate domain versions indexed。
常见问答
Q:我根本没碰过 header,既然 Vercel 会自动加 noindex,我的 preview 怎么还被收录了?
因为自动 header 有几个被官方记录的缺口。Vercel 在自定义域名挂到非生产分支时不会加 X-Robots-Tag: noindex,Netlify 也让最新的 branch deploy 保持可索引。对着 Google 收录的那个确切 host 跑 curl -I,如果没有这个头,你就落在这些缺口里,需要走步骤 1。
Q:已被收录的 preview URL 多久能从 Google 掉出来?
Search Console 的 Removals 几小时内隐藏(临时,约 6 个月)。永久去索引要靠 noindex 头 + Google 下次回爬 —— 每条通常 1-4 周,低流量老 URL 更久。
Q:要不要把 preview URL 301 重定向到正式域名?
对你能控制的自定义域名 staging host,要做 —— 301 到正式对应路径能传递权重、消除重复页。对你加不了按路径 redirect 的系统生成 *.vercel.app / *.netlify.app host,就靠 noindex + Removals。
Q:preview 加 noindex 会不会影响平台自己的部署检查?
不会。Vercel 和 Netlify 的部署检查看的是 HTTP 状态码(200),不看 robots 指令。部署 bot 会忽略 X-Robots-Tag。
Q:自定义分支别名(比如 staging.your-site.com)也泄漏了,同一套修法?
是的,而且这是 2026 年最常见的一种。自定义域名挂在非生产分支上,正是 Vercel 不加 noindex 的场景。按 preview 处理:显式 noindex 头 + Deployment Protection,或者 301 到正式域名。