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.slug 和 type: "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,那不是你这里要的。
如何确认已修好
别只信「看着没了」。四项都要验:
- build 产物干净。clean
npm run build后,dist/里没有草稿正文或 slug:
退出码grep -rl "draft.*true\|\[TODO\|DON'T PUBLISH" dist/ ; echo "exit: $?"1(没匹配)才是你要的。 - URL 在服务器端确实没了。它应返回你选的状态码、而不是 200:
curl -sI https://yoursite.com/articles/leaked-draft/ | head -n1 # 预期:HTTP/2 410 (或 404) - 它正在从 Google 掉下去。把 URL 丢进 Search Console 的 URL Inspection 工具,应报告该页未被索引 / 已移除;
site:yoursite.com/articles/leaked-draft/也应在上面说的去索引窗口内不再返回该页。 - 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 用不同的
DRAFTSenv var;永不互换、永不把一个复制进另一个。 - 留一条
npm run audit:drafts-published,随时能列出当前草稿并 grepdist/。 - 出事后写一份简短时间线。反复泄漏说明有系统性 gap(通常是 Step 2 或 Step 4),把它彻底关掉。