草稿页误发布:6 种泄漏来源 + CI guard 防再发

还标着 `draft: true` 的页面被 build、部署、还被 Google 收录了占位符内容。审计 + 用 410 去索引 + CI 加 guard,让 build 不可能把 draft 发到生产。

Search Console 这周报已索引 URL 突然涨了一截。你一查——是标题为「Untitled」「[TODO write intro]」「Draft notes - DON’T PUBLISH」的文章。你的草稿被 build 进了生产,现在躺在 Google 索引里:啥都不排、给落地的人看占位符文字、还给 Google 一个「这站会发低质页面」的信号。

最快修复(10 分钟):先列出所有泄漏草稿(grep -rln "^draft: true" src/content/),要删的让 URL 返回 HTTP 410 Gone,再确认你的 build 真的会过滤 draft: true、不会再漏新的。然后对已泄漏的去索引,并加上 CI guard。具体步骤在下面,最急的先做。

多数「draft 误发」事件是配置 gap、不是人为失误:build 不认 draft: true、bulk-edit 脚本把 flag 翻了、或者带草稿的 staging build 被升到了 prod。所以稳妥的修法分两半:清理已泄漏的,再让 build 根本不可能把 draft 发进生产。

先判断你属于哪一类

现象最可能的原因跳到
所有草稿全上线、不只是新的build 完全不认 draft原因 1、Step 2
跑完某个 bulk frontmatter 脚本后草稿上线脚本给所有文件写了 draft: false原因 2
只在某一次部署后发生staging build 被升到了 prod原因 3
只有全新文章泄漏新文件模板默认 draft: false原因 4
只有某个作者的草稿泄漏保存时编辑器默认覆盖了 flag原因 5
页面「在线」但 clean rebuild 后就没了CDN / build cache 服务的是旧版本原因 6

常见原因

按命中率从高到低。

1. Build 管道不认 draft: true

你的 content loader 不管 draft flag、所有 .mdx 都包含进来——flag 是装饰,build 里没东西读它。

如何判断:在 codebase 里搜 draft 在哪被消费。

grep -rn "draft" src/content.config.ts src/pages/ astro.config.mjs

build 路径里没有引用,说明 flag 没起作用。

2. bulk-edit 脚本翻了 draft flag

你跑了个 normalize frontmatter 的脚本(「给所有文章加 publishedAt」)。脚本顺手把 draft: false 当默认值写进了每个文件——草稿一夜上线。

如何判断:在最近那次 bulk edit 附近跑 git log --all -p -S "draft: false"。脚本不分青红皂白设 draft: false,就是它。

3. staging 部署被升到生产、草稿搭便车

你 staging 用 DRAFTS=true、prod 用 DRAFTS=false。有人把 staging build 部到了 prod(或者把 staging 的 env 复制进了 prod)——草稿搭了便车。

如何判断:看 prod 部署的 build log。那次 build 里设了 DRAFTS=true,它就不是一个 prod build。

4. 新文件默认 draft: false

你 / CMS 建新文章时模板默认 draft: false——下次部署新文章就上线,哪怕你本想「先存以后写完」。

如何判断:打开文章模板 / 生成器。新文件默认 draft: false,那每篇新文章都会自动发布。

5. 多作者翻 flag 不一致

一个作者用 draft: true 在写,另一个打开文件保存时没注意——他的保存把 flag 替换成了编辑器默认值。作者之间的竞态。

如何判断:对泄漏文章的 draft: 行做 git blame。翻 flag 的是没写这内容的人,那就是误存。

6. build 或 CDN cache 服务的还是上一个 draft: false 版本

你设了 draft: true 并推上去了,但 cache 还在服务上一份 build。文章看着在线,是因为 cache 没失效。

如何判断:强制清 cache / clean rebuild。clean rebuild 后文章消失,就是 cache 问题、不是 build 本身。

最短修复路径

按紧迫度排。Step 1 和 2 加起来约 10 分钟——别等。

Step 1:找出泄漏 URL 并去索引

# 在 repo 里列出当前所有草稿
grep -rln "^draft: true" src/content/

# 在 Search Console 里按占位符标题过滤
# Performance -> Pages,然后在页面列表里筛 "untitled" / "todo" / "draft"

每个泄漏 URL 选一条路:

  • 想留着写完:等真正可以排名了再翻成 draft: false
  • 永久删除:让该 URL 返回 HTTP 410 Gone
  • 临时隐藏:给页面加 noindex + 提交 Search Console removal 请求(见 Step 6)。

永久删除优先用 410 Gone、别用 404 Not Found。截至 2026 年 6 月,Google 把 410 当作明确的「这页没了」信号,通常 1-2 周内丢掉这些 URL;而 404 约 2-4 周(Google 把 404 当作可能只是临时)。爬取频繁的站点 410 几天内就能掉。

Step 2:让 build 认 draft: true(如果它不认)

这才是真正的修复。Step 1、5、6 是清理,这一步才是防复发。

在 Astro 5 里,集合定义放在 src/content.config.ts、用 glob() loader(旧的 type: "content" 配置已经没了):

// src/content.config.ts
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

const articles = defineCollection({
  loader: glob({ pattern: "**/*.mdx", base: "./src/content/articles" }),
  schema: z.object({
    draft: z.boolean().default(false),
    // 其他字段
  }),
});

export const collections = { articles };

只在生产里过滤掉草稿,这样它们在 astro dev 里仍然能渲染:

// src/pages/articles/[id].astro
import { getCollection } from "astro:content";

export async function getStaticPaths() {
  const published = await getCollection("articles", ({ data }) =>
    import.meta.env.PROD ? data.draft !== true : true
  );
  // 注意:Astro 5 用 entry.id(不是 entry.slug)
  return published.map((a) => ({ params: { id: a.id }, props: { article: a } }));
}

草稿现在在 astro dev 里有、在 astro build 输出里没有。如果你还在 Astro 4,旧的 entry.slugtype: "content" API 概念上一样,只是字段名不同。

Step 3:CI 加 guard,拦住 build 产物里的 draft

# .github/workflows/no-drafts-in-prod.yml
on: pull_request
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - name: Block drafts in production output
        run: |
          if grep -rl "draft.*true\|DRAFT\|\[TODO" dist/ 2>/dev/null; then
            echo "::error::生产产物里发现 draft 标记"
            exit 1
          fi

dist/ 产物里有任何 draft 标记,PR 在合并前就 fail。要 grep 整个 dist/ 目录、不只是 sitemap,这样占位符正文也能被抓到。

Step 4:新文章默认 draft: true

在 CMS 或 scaffold 模板里:

---
title: ""
draft: true
publishedAt:
---

作者必须主动翻成 draft: false 才会发布。opt-in、不是 opt-out。光这一改就能挡掉最常见的泄漏(原因 4 的新文件默认)。

Step 5:修完后强制清 cache

如果 CDN 缓存了泄漏 URL,在 cache 清掉之前,去索引对新请求不可见:

# Cloudflare 全量清除
curl -X POST "https://api.cloudflare.com/client/v4/zones/<zone>/purge_cache" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -d '{"purge_everything":true}'

# Vercel:不复用 build cache 重新部署
vercel deploy --prod --force

Step 6:在 Search Console 申请临时 removal

410 / noindex 通过爬取慢慢生效期间,removal 工具是条快车道。

Search Console -> Removals -> 「Temporary Removals」标签页 -> New Request
- 选「Temporarily remove URL」
- 填每个泄漏 draft 的 URL,确认范围,点 Next

重点,且截至 2026 年 6 月已更正:一次成功的临时 removal 大约持续6 个月(Google 已经弃用了旧的「90 天」说法)。它只是把 URL 从结果里隐藏、并不从索引里删除。所以这 6 个月里你必须补一个永久信号——把页面写完,或返回 410 / 加 noindex——否则 removal 过期后 URL 会重新出现。另外还有个独立的「Clear snippet in search」选项,它只清描述 snippet,那不是你这里要的。

如何确认已修好

别只信「看着没了」。四项都要验:

  1. build 产物干净。clean npm run build 后,dist/ 里没有草稿正文或 slug:
    grep -rl "draft.*true\|\[TODO\|DON'T PUBLISH" dist/ ; echo "exit: $?"
    退出码 1(没匹配)才是你要的。
  2. URL 在服务器端确实没了。它应返回你选的状态码、而不是 200:
    curl -sI https://yoursite.com/articles/leaked-draft/ | head -n1
    # 预期:HTTP/2 410   (或 404)
  3. 它正在从 Google 掉下去。把 URL 丢进 Search Console 的 URL Inspection 工具,应报告该页未被索引 / 已移除;site:yoursite.com/articles/leaked-draft/ 也应在上面说的去索引窗口内不再返回该页。
  4. guard 真的管用。开一个一次性 PR,加一个 draft: true 的文件,确认 Step 3 的 CI job 把它 fail 掉。

常见问题

该用 410、404 还是 noindex? 草稿永不再回就用 410 Gone——去索引最快(截至 2026 年 6 月约 1-2 周)。想保留 URL 但让它隐形(比如以后会写完)就用 noindex。只有当你的技术栈不方便发 410 时才用 404,它也能用,但 Google 因为把 404 当作可能临时、移除得更慢。

泄漏的页面多久从 Google 掉出去? 410 通常 1-2 周;404 约 2-4 周;爬取频繁的站点更快。Search Console 临时 removal 能在几小时内隐藏,但只持续约 6 个月,所以要配一个永久信号。

removal 工具说能持续 6 个月——这算永久修好了吗? 不算。它只把 URL 从搜索结果里隐藏、不从索引里删任何东西。如果你不同时发 410/noindex 或把页面写完,removal 过期后它会重现。把 removal 工具当应急、不是修复本身。

我的草稿在 astro build 里被过滤了,但线上还能看到,为啥? 几乎都是 cache(原因 6)。强制 CDN 全清 + clean 重部(Step 5),再用 curl -sI 复查。如果新请求仍返回 200,说明 build 还在产出它——回去检查 Step 2 的 getCollection 过滤。

草稿被索引过,长期会伤排名吗? 快速清理掉的短暂泄漏风险很低。真正的问题是索引里长期存在的低质占位符页面,它们会拉低全站的质量信号。Step 2-4 的意义就是确保这是一次性的小插曲、不是反复出现的模式。

我能不能直接删文件就完事? 删源文件能让以后的 build 不再产出它们,但已经在 Google 索引里的 URL 需要返回 410/404(让 Google 看到它们没了),最好再配一个 removal 请求加速。只删文件,会让已索引的 URL 返回你 host 的某个默认响应。

预防建议

  • build 必须认 draft: true,并用 CI check 断言没有草稿进 dist/(Step 3)。
  • 新文章模板默认 draft: true——opt-in 发布、不是 opt-out。
  • 改 frontmatter 的 bulk-edit 脚本必须显式保留已有的 draft: true,绝不一刀切写 draft: false
  • staging 和 prod 用不同的 DRAFTS env var;永不互换、永不把一个复制进另一个。
  • 留一条 npm run audit:drafts-published,随时能列出当前草稿并 grep dist/
  • 出事后写一份简短时间线。反复泄漏说明有系统性 gap(通常是 Step 2 或 Step 4),把它彻底关掉。

相关阅读

标签: #内容运营 #站点质量 #站点审计 #排查 #草稿误发布