← Field Notes
Published 8 min read

How I Kept Drafts Out of Astro Production Builds

The layered controls I use so unfinished Markdown can stay in the repository without reaching pages, feeds, search, or build output.

In this article
  1. The directory is the first boundary
  2. The production collection excludes drafts before downstream features see them
  3. Frontmatter remains useful as a second signal
  4. A successful build is not enough
  5. Assets can leak even when Markdown does not
  6. Repository privacy is not the same as publication control
  7. Publishing is an explicit state transition
  8. Why I prefer exclusion over repeated filtering
  9. The checklist I use after changing publishing logic
  10. Source boundary
  11. Downstream consumers
  12. Negative output tests
  13. Asset review
  14. Release validation
  15. What I would do differently
  16. References
  17. Security note
  18. AI transparency

draft: true is useful metadata. It is not the publication boundary.

When I started building a backlog for Eldritch IT Field Notes, I wanted unfinished articles to remain close to the site instead of living in a disconnected notes app.

That made editing easier, but it created a more important question:

How do I let a draft exist in the repository without allowing it to leak into the production site?

A single frontmatter flag did not feel strong enough. The site has article pages, category pages, archives, related-post logic, series navigation, search, and RSS. If every feature had to remember to filter draft: true, then publication control would become a distributed policy.

Distributed policies drift.

I chose a layered approach instead:

  1. Drafts live in a dedicated directory.
  2. The production content collection excludes that directory.
  3. Draft frontmatter still records editorial state.
  4. Published content requires a publication date.
  5. Production output is checked for absence, not only successful generation.
  6. Draft assets receive the same privacy review as draft text.

No single layer is expected to carry the entire job.

The directory is the first boundary

Every unpublished Field Notes article lives under a dedicated drafts directory.

That makes status visible before the file is even opened. A path communicates intent immediately:

src/content/blog/drafts/example-article.md

The separation also prevents drafts from being mixed into the normal article tree and then filtered independently by every consumer.

That matters because a content-heavy site rarely has only one consumer. A post may appear in:

  • Its own route
  • The homepage
  • Category and tag pages
  • Archive lists
  • Related-post sections
  • Series navigation
  • RSS
  • Search data
  • Sitemaps

If each feature scans Markdown separately, each one becomes another place where a forgotten condition can publish unfinished work.

The directory is therefore more than organization. It is a coarse publication boundary.

The production collection excludes drafts before downstream features see them

Field Notes uses Astro’s content collections and the built-in glob() loader.

The production collection includes Markdown and MDX under the blog directory while explicitly excluding the drafts subtree:

const blog = defineCollection({
  loader: glob({
    base: "./src/content/blog",
    pattern: ["**/*.{md,mdx}", "!drafts/**/*"],
  }),
  // schema omitted here
});

Astro’s current content-loader documentation defines pattern as a string or array of glob patterns resolved relative to the loader’s base. That makes exclusion part of the collection definition rather than a convention repeated across templates.

This is the most important control in the design.

A draft does not enter the production collection and then get hidden later. It never becomes production content in the first place.

That means pages, feeds, search, archives, and related-post logic can all consume the same collection without each feature implementing its own draft policy.

The practical rule is simple:

Downstream features should receive already-approved content, not raw files plus a reminder to filter them.

Frontmatter remains useful as a second signal

Draft files still declare their editorial state:

draft: true

I also omit pubDate until publication.

Those fields are not redundant. They serve tools and humans that may inspect files outside Astro’s production collection.

The path answers, “Where does this file belong?”

The frontmatter answers, “What state is this article in?”

The missing publication date answers, “Has this article completed the release step?”

That combination makes backlog audits clearer and reduces the chance that moving a file alone is mistaken for a complete publication process.

The production schema reinforces the distinction. Published entries must provide a valid publication date, so a draft moved into the public tree prematurely should fail validation instead of silently appearing with incomplete metadata.

A successful build is not enough

A passing build proves that Astro could generate the site.

It does not prove that unpublished content stayed out.

This is an important difference between positive and negative testing.

A positive test asks:

  • Did the expected article route build?
  • Did RSS generate?
  • Did the archive page render?

A negative test asks:

  • Was a draft route generated?
  • Does the output contain a draft title?
  • Did a draft-only phrase enter search data?
  • Did an unpublished article reach RSS or a sitemap?

The expected result is absence, so the validation process has to search for something that should not exist.

After changing content-loading or publishing logic, I inspect the generated output for:

  • Draft directory names
  • Known draft titles
  • Distinctive draft-only phrases
  • Unexpected routes
  • Draft references in feeds, search indexes, or sitemaps

This check is especially valuable after refactoring. A site can build perfectly while a new feature bypasses the intended collection boundary.

Assets can leak even when Markdown does not

Excluding a Markdown file does not automatically make every file it references private.

Astro’s current documentation states that files placed in public/ are served or copied into the build output as-is. They do not need a published article to link to them before they become directly reachable.

That makes draft assets a separate review surface.

A screenshot, diagram, archive, or downloadable configuration can expose information even when its article never receives a route.

Before an asset enters the repository, I review it for:

  • Hostnames and internal domains
  • IP addresses and network ranges
  • Usernames and account identifiers
  • Tokens, keys, cookies, or QR codes
  • Internal paths and share names
  • Device inventories
  • Browser tabs, bookmarks, and notifications
  • Metadata that identifies a person, client, or location

The safest draft asset is already sanitized before it enters source control.

Publication controls and information-security controls solve different problems. A private repository does not remove the need for sanitization, and an excluded content directory does not protect files copied into public output.

Repository privacy is not the same as publication control

Field Notes is managed in a private repository, but I do not treat privacy settings as a permanent security boundary.

Repository visibility can change. Access can expand. Files can be copied into build systems, backups, local clones, or third-party tools. Git history can retain content after it is removed from the latest version.

That is why I generalize sensitive details in drafts as aggressively as I do in published posts.

I avoid storing real infrastructure specifics unless they are necessary, safe, and intentionally public. Examples use generic roles instead of actual systems:

media-host
storage-host
reverse-proxy
application-container

The article remains useful while the environment stays private.

Publishing is an explicit state transition

I do not consider an article published merely because its file moved out of the drafts directory.

The release step includes:

  1. A final technical and editorial review
  2. Verification of time-sensitive claims against primary documentation
  3. A privacy and security pass
  4. Setting the publication date
  5. Removing draft-only metadata
  6. Moving the article into the production collection
  7. Checking internal links and referenced assets
  8. Building the production site
  9. Confirming that the expected route exists
  10. Confirming that no draft-only content escaped with it

That sequence turns publication into an intentional state transition rather than a filesystem accident.

It also gives me a clean failure mode. If the article is not ready, it stays outside the production content graph.

Why I prefer exclusion over repeated filtering

Filtering draft: true at query time can work.

For a small site with one list and one route generator, it may be perfectly reasonable.

The risk grows as the site adds more consumers. Every new page, feed, index, or integration must remember the same rule. The implementation can become inconsistent without anyone noticing immediately.

Excluding drafts at the collection boundary reduces the number of places that need to understand editorial state.

It also makes the architecture easier to explain:

Draft files
    ↓ excluded by loader
Production collection
    ↓ shared by site features
Pages / RSS / search / archives / series

That is simpler than passing drafts everywhere and asking each feature to hide them correctly.

The checklist I use after changing publishing logic

When I modify the content schema, loader, route generation, feed, search, or sitemap behavior, I check the following:

Source boundary

  • Are all unfinished articles still under the drafts directory?
  • Does the production loader explicitly exclude that directory?
  • Does the published schema require the metadata needed for release?

Downstream consumers

  • Do article lists use the production collection?
  • Do RSS, search, archives, categories, tags, and series use that same source?
  • Is any feature scanning the filesystem independently?

Negative output tests

  • Did any draft route build?
  • Does the output contain known draft titles or phrases?
  • Are drafts absent from RSS, search, sitemaps, and navigation?

Asset review

  • Are draft assets outside public output unless intentionally public?
  • Have screenshots and downloads been sanitized?
  • Do removed sensitive files remain in repository history?

Release validation

  • Does the published article have a date and complete metadata?
  • Do its internal links resolve?
  • Does the expected route appear after the build?
  • Did exactly the intended article become public?

What I would do differently

I would have designed this boundary before creating the backlog.

Retrofitting draft filters after several site features already consume content is easy to get wrong. Every feature becomes another audit target, and a missing condition may remain invisible until a draft appears somewhere unexpected.

A dedicated directory plus one production collection boundary is easier to reason about, easier to test, and easier to explain to future me.

The goal is not to hide drafts after they enter the site.

The goal is to keep them out of the production content graph until publication is a deliberate change.

For more on the underlying site architecture, see Why I Chose Astro and Cloudflare for EldritchIT.me.

References

Security note

Code samples show only the publication pattern used by Field Notes. Draft titles, internal tooling details, deployment identifiers, repository credentials, infrastructure names, addresses, account identifiers, private paths, and unsanitized assets are intentionally omitted.

AI transparency

AI assisted with structure, copy editing, and checking current Astro documentation. The directory layout, collection exclusion, schema behavior, output checks, and publication workflow are based on the Field Notes implementation.

JO

Written by

Jessie Owens

I run Eldritch IT and write about the systems, repairs, infrastructure decisions, and business lessons behind the work.