Skip to main content

DIY Draft System for Eleventy v3

The Problem #

Every blogging platform has drafts — unpublished posts that only the author can see. WordPress has them, Ghost has them, even Jekyll has published: false. But Eleventy v3? Nothing built-in.

In Eleventy v2, you could rely on the future option to skip posts with future dates. In v3, that option doesn’t exist. Every post in your content/ directory is fair game, regardless of its date.

This became painfully obvious when I scheduled a post for tomorrow, deployed, and found it live immediately. A blog without draft support is a blog where every mistake is instantly public.

The Requirements #

I wanted a draft system that checks three boxes:

  1. Zero public visibility — drafts must not appear in any list, feed, tag page, or sitemap
  2. Previewable — I should be able to visit a draft’s URL directly to proofread
  3. One-step publish — no file moving, no folder renaming, just flip a switch

The Implementation #

Eleventy v3 provides all the hooks you need. The solution uses two official APIs working together.

The permalink function in a data file can access frontmatter. When draft: true, we route the post to a /drafts/ path instead of /posts/:

// content/posts/posts.11tydata.js export default { tags: ["posts"], layout: "layouts/post.njk", permalink: ({ draft, page }) => draft ? `/drafts/${page.fileSlug}/` : `/posts/${page.fileSlug}/`, // ... };

This means a draft lives at /drafts/my-post/ while a published post lives at /posts/my-post/. The URL changes automatically when you toggle the draft flag.

Step 2: Exclude from All Collections #

The eleventyExcludeFromCollections property removes an item from every single collection — the posts collection, tag-specific collections like collections.eleventy, and even collections.all. Without this, a draft would still appear on tag pages and in the RSS feed.

But eleventyExcludeFromCollections is a static boolean. To make it dynamic based on frontmatter, we use eleventyComputed:

// content/posts/posts.11tydata.js export default { // ... eleventyComputed: { eleventyExcludeFromCollections: ({ draft }) => draft ? true : undefined, }, };

The function returns true only when draft: true. Otherwise it returns undefined, leaving the default behavior intact.

The Complete Data File #

Here’s the full posts.11tydata.js that powers every post on this site:

export default { tags: ["posts"], layout: "layouts/post.njk", permalink: ({ draft, page }) => draft ? `/drafts/${page.fileSlug}/` : `/posts/${page.fileSlug}/`, eleventyComputed: { eleventyExcludeFromCollections: ({ draft }) => draft ? true : undefined, }, };

Twenty lines. No plugins. No external dependencies.

How Publishing Works #

To write a new draft, add draft: true to the frontmatter:

--- title: "My Upcoming Post" date: 2026-06-20 draft: true tags: [some-topic] ---

When you build or deploy, the post is:

AspectBehavior
File location/drafts/my-upcoming-post/
HomepageHidden
Tag pagesHidden
RSS feedHidden
Next/prev navHidden
SitemapHidden
Direct URL access✅ Works

To publish, delete the draft: true line, commit, and push:

git add content/posts/my-upcoming-post.md git commit -m "Publish: My Upcoming Post" git push

The post moves from /drafts/ to /posts/ and appears everywhere it should. No files to move, no folders to rename, no config toggles to remember.

Why Not _drafts Folder? #

Eleventy v3 (and v2) automatically ignores any directory starting with an underscore. A content/_drafts/ folder works without any configuration — files inside it are never processed.

But the folder approach has tradeoffs:

Aspect_drafts/ folderdraft: true frontmatter
SetupZero config20 lines in data file
PreviewImpossible (no URL)/drafts/slug/
Publishgit mv between foldersDelete one line
Content organizationSplit across two foldersAll posts in one place
Accidental publishFile move = publish riskToggle is explicit

For a blog where I write on my phone and want to preview before publishing, the frontmatter approach wins. The preview URL alone saves me from countless broken drafts.

What About Scheduled Publishing? #

The current system requires a manual step to publish. For automated scheduling — write a post, set a date, and have it go live automatically — you’d need a CI/CD cron job that checks dates and conditionally removes draft: true.

That’s possible with GitHub Actions, but it adds complexity. For now, a manual git push on the publish date is a ritual I don’t mind. There’s something satisfying about flipping the switch yourself.


This entire site is open source on GitHub. The draft system is in content/posts/posts.11tydata.js — fork it, tweak it, make it yours.