Draft Pages Accidentally Published

Build deployed pages still marked `draft: true`. They got indexed with placeholder text. Audit, deindex, then add CI guards so it can't happen again.

Search Console reports a 15% spike in indexed URLs this week. You investigate — they’re articles titled “Untitled”, “[TODO write intro]”, “Draft notes - DON’T PUBLISH”. Your draft articles got built into production. They’re now in Google’s index, ranking for nothing useful, signaling to Google that your site has low-quality content, and presenting placeholder text to anyone who finds them.

Most “draft published” incidents are configuration gaps, not human mistakes. Your build doesn’t respect draft: true, a bulk-edit script flipped flags, or staging got promoted to prod with drafts included. The fix: audit + deindex what leaked, then add CI guards so the build cannot produce a draft in production.

Common causes

Ordered by hit rate, highest first.

1. Build pipeline doesn’t respect draft: true frontmatter

Your content loader includes all .mdx files regardless of draft flag. The flag is decorative; nothing reads it.

How to spot it: Search your codebase for how draft is handled:

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

If nothing references it in the build path, 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" near 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, DRAFTS=false on prod. Someone deployed the staging build to prod (or copied the staging env to prod). Drafts hitchhiked.

How to spot it: Check the production deploy’s build logs. If DRAFTS=true was set, that build wasn’t a prod build.

4. Default frontmatter for new files is draft: false

When you / your CMS create a new article, the template defaults draft: false. New articles ship to production immediately upon any deploy, even if you intended to “save and finish later.”

How to spot it: Look at the article template / generator. If new files default to draft: false, every new article auto-publishes.

5. Multiple agents (or co-authors) flipped flags inconsistently

One author edits with draft: true, another author opens the file and saves without noticing — their save replaces the flag with the editor’s default. Race condition between authors.

How to spot it: git blame on the draft: line of leaked articles. If the flag flip is by someone who didn’t author the content, accidental save.

6. Build cache served the previous “draft: false” version

You set draft: true and pushed; the build cache still served the previous build. Article appears live because cache hasn’t invalidated.

How to spot it: Force a cache purge / rebuild. If the article disappears after a clean rebuild, cache was the issue, not the build itself.

Shortest path to fix

Ordered by urgency. Steps 1 and 2 take 10 minutes; don’t wait.

Step 1: Find and deindex the leaked URLs

# In your CMS / repo: list all current drafts
grep -rln "^draft: true" src/content/articles/

# In Search Console: filter for placeholder titles
# (Performance → Pages → search "untitled" / "todo" / "draft")

For each leaked URL:

- If you want to keep + finish it: update to `draft: false` once it's ready
- If you want to remove it: 410 the URL (return HTTP 410 Gone)
- For temporary hiding: noindex + Search Console removal request

Use 410 for permanent removal — it’s faster than 404 for getting Google to drop.

Step 2: Make your build respect draft: true (if it doesn’t)

In Astro:

// src/content/config.ts
const articles = defineCollection({
  type: "content",
  schema: z.object({
    draft: z.boolean().default(false),
    // ... other fields
  }),
});

In page loader:

// src/pages/articles/[slug].astro
export async function getStaticPaths() {
  const all = await getCollection("articles");
  const published = all.filter(a => !a.data.draft || import.meta.env.DEV);
  return published.map(a => ({ params: { slug: a.slug }, props: { article: a } }));
}

Drafts now exist in dev (pnpm dev) but never in pnpm build output.

Step 3: Add a CI guard against drafts in sitemap

# .github/workflows/no-drafts-in-prod.yml
on: pull_request
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pnpm install && pnpm build
      - name: Block drafts in production output
        run: |
          if grep -l "draft.*true\|DRAFT\|TODO" dist/sitemap-index.xml dist/**/*.html 2>/dev/null; then
            echo "::error::Draft markers found in production build"
            exit 1
          fi

Anything containing draft markers in production output fails the PR.

Step 4: Make draft: true the default for new articles

In your CMS / scaffold:

---
title: ""
draft: true   ← default
publishedAt:
---

Authors must consciously flip draft: false to publish — opt-in, not opt-out.

Step 5: Force cache purge after fixing

If your CDN cached the leaked URLs:

# 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 with no cache
vercel deploy --prod --force

Until cache clears, the deindex isn’t visible to fresh requests.

Step 6: For accidentally indexed pages, request Search Console removal

Search Console → Removals → New Request
- Submit URL of each leaked draft
- Choose "Temporarily hide URL" (90 days)
- During that 90 days, either complete the page or 410 it

Faster than waiting for re-crawl. Don’t skip — leaked drafts hurting site quality is worth the few minutes.

Prevention

  • Build must respect draft: true — assert with a CI check that no drafts reach dist/ or the sitemap
  • New article template defaults to draft: true — opt-in publishing, not opt-out
  • Bulk-edit scripts that modify frontmatter must explicitly preserve draft: true flags
  • Staging and prod environments use distinct DRAFTS env vars; never swap them
  • Add a quick pnpm audit:drafts-published script you can run anytime
  • After incidents, document the timeline; recurring leaks reveal a systemic gap to close

Tags: #Content ops #Site quality #Site audit #Troubleshooting #Draft published