Search Console reports a spike in indexed URLs this week. You investigate and find articles titled “Untitled”, “[TODO write intro]”, “Draft notes - DON’T PUBLISH”. Your draft articles got built into production. They’re in Google’s index now, ranking for nothing, showing placeholder text to anyone who lands on them, and signaling to Google that your site ships low-quality pages.
Fastest fix (10 minutes): list every leaked draft (grep -rln "^draft: true" src/content/), return HTTP 410 Gone for the ones you want gone, and confirm your build actually filters draft: true so no new ones leak. Then deindex what already leaked and add a CI guard. Steps below, urgent first.
Most “draft published” incidents are configuration gaps, not human mistakes. The build doesn’t respect draft: true, a bulk-edit script flipped the flags, or a staging build got promoted to prod with drafts included. So the durable fix has two halves: clean up what leaked, and make the build incapable of emitting a draft into production.
Which bucket are you in?
| Symptom | Most likely cause | Jump to |
|---|---|---|
| Every draft is live, not just new ones | Build ignores draft entirely | Cause 1, Step 2 |
| Drafts went live right after a bulk frontmatter script ran | Script wrote draft: false to all files | Cause 2 |
| Only happened after one specific deploy | Staging build promoted to prod | Cause 3 |
| Only brand-new articles leak | New-file template defaults to draft: false | Cause 4 |
| One author’s drafts leak, not others’ | Editor default overwrote the flag on save | Cause 5 |
| Page is “live” but a clean rebuild drops it | CDN/build cache served the old version | Cause 6 |
Common causes
Ordered by hit rate, highest first.
1. Build pipeline doesn’t respect draft: true frontmatter
Your content loader includes every .mdx file regardless of the draft flag. The flag is decorative; nothing in the build reads it.
How to spot it: search your codebase for where draft is consumed.
grep -rn "draft" src/content.config.ts src/pages/ astro.config.mjs
If nothing in the build path references it, the flag does nothing.
2. A bulk-edit script overwrote draft flags
You ran a script to normalize frontmatter (“add publishedAt to all articles”). The script also wrote draft: false to every file as a default. Drafts went live overnight.
How to spot it: git log --all -p -S "draft: false" around your recent bulk edit. If the script set draft: false indiscriminately, you found it.
3. Staging deploy got promoted to production with drafts
You build with DRAFTS=true on staging and DRAFTS=false on prod. Someone deployed the staging build to prod (or copied the staging env into prod). Drafts hitchhiked.
How to spot it: check the production deploy’s build logs. If DRAFTS=true was set in that build, it wasn’t a prod build.
4. Default frontmatter for new files is draft: false
When you or your CMS create a new article, the template defaults to draft: false. New articles ship on the next deploy even if you meant to “save and finish later.”
How to spot it: open the article template / generator. If new files default to draft: false, every new article auto-publishes.
5. Multiple authors flipped flags inconsistently
One author edits with draft: true; another opens the file and saves without noticing, and their save replaces the flag with the editor’s default. A race between authors.
How to spot it: git blame the draft: line of leaked articles. If the flip is by someone who didn’t write the content, it was an accidental save.
6. Build or CDN cache served the previous draft: false version
You set draft: true and pushed, but the cache still serves the previous build. The article looks live because the cache hasn’t invalidated.
How to spot it: force a cache purge / clean rebuild. If the article disappears after a clean rebuild, cache was the issue, not the build.
Shortest path to fix
Ordered by urgency. Steps 1 and 2 take about 10 minutes — don’t wait.
Step 1: Find and deindex the leaked URLs
# In your repo: list every current draft
grep -rln "^draft: true" src/content/
# In Search Console: filter for placeholder titles
# Performance -> Pages, then filter the page list for "untitled" / "todo" / "draft"
For each leaked URL, pick one path:
- Keep and finish it: flip to
draft: falseonly once it’s actually ready to rank. - Remove it permanently: make the URL return HTTP
410 Gone. - Hide it temporarily:
noindexthe page plus a Search Console removal request (Step 6).
Prefer 410 Gone over 404 Not Found for permanent removal. As of June 2026, Google treats 410 as a definitive “this is gone” signal and tends to drop those URLs in roughly 1-2 weeks, versus about 2-4 weeks for 404 (which Google treats as possibly temporary). Frequently crawled sites can see 410 drops in a few days.
Step 2: Make your build respect draft: true (if it doesn’t)
This is the actual fix. Steps 1, 5, and 6 are cleanup; this is what stops recurrence.
In Astro 5, the collection lives in src/content.config.ts and uses the glob() loader (the legacy type: "content" config is gone):
// 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),
// ... other fields
}),
});
export const collections = { articles };
Filter drafts out in production only, so they still render in 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
);
// Note: Astro 5 uses entry.id (not entry.slug)
return published.map((a) => ({ params: { id: a.id }, props: { article: a } }));
}
Drafts now exist in astro dev but never appear in astro build output. If you’re still on Astro 4, the older entry.slug and type: "content" API works the same way conceptually; only the field names differ.
Step 3: Add a CI guard against drafts in the build output
# .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 markers found in production build"
exit 1
fi
Any draft marker in the dist/ output fails the PR before it can merge. Grep the whole dist/ tree, not just the sitemap, so placeholder bodies are caught too.
Step 4: Make draft: true the default for new articles
In your CMS or scaffold template:
---
title: ""
draft: true
publishedAt:
---
Authors must consciously flip to draft: false to publish. Opt-in, not opt-out. This single change prevents the most common leak (new-file default in Cause 4).
Step 5: Force a cache purge after fixing
If your CDN cached the leaked URLs, the deindex won’t be visible to fresh requests until the cache clears:
# Cloudflare full purge
curl -X POST "https://api.cloudflare.com/client/v4/zones/<zone>/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-d '{"purge_everything":true}'
# Vercel: redeploy without reusing build cache
vercel deploy --prod --force
Step 6: Request temporary removal in Search Console
The removal tool is the fast lane while the 410/noindex propagates through crawling.
Search Console -> Removals -> "Temporary Removals" tab -> New Request
- Choose "Temporarily remove URL"
- Enter the URL of each leaked draft, confirm scope, click Next
Important, and corrected as of June 2026: a successful temporary removal lasts about six months (Google retired the old “90 days” wording). It only hides the URL from results; it does not delete it from the index. So during those six months you must apply a permanent signal — finish the page, or return 410 / add noindex — or the URL reappears when the removal expires. There is also a separate “Clear snippet in search” option that only wipes the description snippet; that is not what you want here.
How to confirm it’s fixed
Don’t trust “looks gone.” Verify all four:
- Build output is clean. After a clean
npm run build, no draft body or slug is indist/:
Exitgrep -rl "draft.*true\|\[TODO\|DON'T PUBLISH" dist/ ; echo "exit: $?"1(no matches) is what you want. - The URL is actually gone from the server. It should return the status you chose, not 200:
curl -sI https://yoursite.com/articles/leaked-draft/ | head -n1 # expect: HTTP/2 410 (or 404) - It’s dropping from Google. Run the URL through the Search Console URL Inspection tool; it should report the page as not indexed / removed, and
site:yoursite.com/articles/leaked-draft/should stop returning the page within the deindex window above. - The guard works. Open a throwaway PR that adds a file with
draft: trueand confirm the CI job in Step 3 fails it.
FAQ
Should I use 410, 404, or noindex?
Use 410 Gone if the draft should never come back — it deindexes fastest (roughly 1-2 weeks as of June 2026). Use noindex if you want to keep the URL live but invisible (e.g., you’ll finish the article later). Use 404 only if your stack can’t easily emit 410; it works but Google removes it more slowly because it treats 404 as possibly temporary.
How long until the leaked pages drop out of Google?
With 410, typically 1-2 weeks; with 404, about 2-4 weeks; frequently crawled sites can be faster. A Search Console temporary removal hides them within hours, but only for about six months, so pair it with a permanent signal.
The removal tool says it lasts 6 months — is that a permanent fix?
No. It only hides the URL from search results and clears nothing from the index. If you don’t also serve 410/noindex or finish the page, it reappears after the removal expires. Treat the removal tool as a stopgap, not the fix.
My draft is filtered in astro build but still shows on the live site. Why?
Almost always cache (Cause 6). Force a CDN purge and a clean redeploy (Step 5), then re-check with curl -sI. If a fresh request still returns 200, the build is still emitting it — recheck the getCollection filter in Step 2.
Will having had drafts indexed hurt my rankings long-term? A short-lived leak that you clean up quickly is low-risk. Sustained low-quality, placeholder pages in the index are the real problem because they pull down site-wide quality signals. The point of Steps 2-4 is to make sure it’s a one-time blip, not a pattern.
Can I just delete the files and move on?
Deleting the source files stops future builds from emitting them, but URLs already in Google’s index need a 410/404 response (so Google sees they’re gone) plus, ideally, a removal request to speed it up. Deleting alone leaves indexed URLs returning whatever your host’s default is.
Prevention
- Build must respect
draft: true, asserted by a CI check that no draft reachesdist/(Step 3). - New-article template defaults to
draft: true— opt-in publishing, not opt-out. - Bulk-edit scripts that touch frontmatter must explicitly preserve existing
draft: trueflags, never blanket-writedraft: false. - Staging and prod use distinct
DRAFTSenv vars; never swap or copy one into the other. - Keep a one-command
npm run audit:drafts-publishedyou can run anytime to list current drafts and grepdist/. - After an incident, write a short timeline. Recurring leaks point at a systemic gap (usually Step 2 or Step 4) to close for good.
Related
Tags: #Content ops #Site quality #Site audit #Troubleshooting #Draft published