部署 preview URL 被搜索引擎收录 —— 排查与修复

Vercel 或 Netlify 的 preview URL 出现在 Google 索引里,甚至盖过你的正式域名,几乎都是 preview host 上漏了 noindex 头或 robots 屏蔽。

site:vercel.app your-project 一搜,发现一堆 preview URL 被 Google 收录了:your-site-git-feature-x.vercel.appyour-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-projectsite:netlify.app your-project(或你的 provider 后缀)搜一下,记下数量。
  • Search Console 里看有没有 “Duplicate without user-selected canonical” 或 “Crawled - currently not indexed” 警告。
  • 确认你有 provider 后台 “Deployment Protection” 的权限。
  • 确认 preview 构建时 process.env.VERCEL_URLprocess.env.URL 解析成什么(每次部署都变)。
  • 确认仓库是公开还是私有,这会显著改变泄漏面积。

需要收集的信息

  • 抽 5 条被收录的 preview URL 及其对应的 deployment ID。
  • 一个 preview URL 的 curl -I 输出,看 x-robots-tagx-vercel-deployment-url
  • canonical URL 生成代码(grep canonicalog:urlimport.meta.env.SITEVERCEL_URL)。
  • sitemap 生成代码(常在 scripts/build-sitemap.mjs 或自动生成)。
  • provider 的 deployment protection 设置。
  • Search Console “Pages” 报告按 vercel.appnetlify.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 索引里的:

  1. Search Console → Removals → New Request → URL prefix https://your-site-abc123.vercel.app/
  2. 提交。Removals 是临时的(6 个月),但立刻生效。
  3. 想永久清,再让那些 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 的 noindexvercel.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。

标签: #排查 #SEO #preview #Vercel #netlify