Last week, I was helping someone over email and we began talking about how to support "draft" posts in Eleventy. A draft post, at least for the purpose of this post, refers to content that should not be rendered on the published site. It may be a blog post that needs time for edits or perhaps just something the author needs time to chew over. It may be committed to a repository, but should not be displayed on the actual website. I did a bit of digging into this and came up with a couple of different solutions I'd like to share, but as always, please let me know what you think or how you've accomplished the same thing.

For this demo, I'm using a simple blog application where posts are all stored in a posts directory. I've got two posts, alpha.md and beta.md, and my intent is for beta.md to be a draft. Initially, each blog post contains front matter for title and layout. Alright, let's get into it!

Iteration One #

The first, and simplest, way to suppress content is to use the permalink feature to disable writing to disk. So given a post I want to be in draft mode, I could just do this:

---
title: beta
layout: main
permalink: false
---

this is beta

Setting permalink to false will stop the file from being published. Sweet and simple, right? But what if you are using a directory data file to specify your collection, or getFilteredByGlob? In that case, the template would still be considered part of the collection even though it wasn't stored. So for example, imagine our posts directory had posts.11tydata.json:

{
"tags":"posts"
}

You would normally do this to save yourself from having to tag every blog post. If you had done this and then iterated over collections.posts, the draft post would show up. The url property would be false, but it would still show up. As an example:

{% for post in collections.posts %}
a post: {{ post.url }}, title {{ post.data.title}}, tags: {{ post.data.tags }}<br/>
{% endfor %}

And the result:

<h2>Posts</h2>

a post: /posts/alpha/, title alpha, tags: posts<br/>
a post: false, title beta, tags: posts<br/>

The other issue I have with this iteration is that - while it halfway works - I don't like the fact that a new developer to the repository may not get why we're using permalink like this. Yes, I'm being picky, but the code isn't being clear about what it's trying to do here. Let's make it better.

Iteration Two #

In the second iteration, I want to fix two problems from the previous version. First, I want to use better front matter to more clearly express my intent, and more importantly, ensure the draft post isn't showing up in collections.

First, let's switch from using a data JSON file to a data JavaScript file so we can use some code. I renamed my JSON file to posts.11tydata.js and used this code:

module.exports = {
    eleventyComputed: {
        permalink: data => {
            if(data.draft) return false;
        },
        tags: data => {
            if(!data.draft) return 'posts';
            return '';
        }
    }
}

I'm making the permalink value dynamic based on the value of draft in front matter. The tags value as well is dynamic, this time only setting it to posts when draft isn't being used. In beta.md, I switched to this:

---
title: beta
layout: main
draft: true
---

this is beta

This looked perfect, but something interesting happened. First, permalink worked perfectly, beta.md wasn't published. But, when I looped over collections.posts - oddly nothing was output! In fact, check this out:

<h2>posts</h2> 

{% for post in collections.posts %}
a post: {{ post.url }}, title {{ post.data.title}}, tags: {{ post.data.tags }}<br/>
{% endfor %}

<h2>all</h2>

{% for post in collections.all %}
page: {{ post.url }}, title {{ post.data.title}}, tags: {{ post.data.tags }}<br/>
{% endfor %}

As you can see, I'm looping over my posts and the global all collection. In both, I output the url, title, and tags. Here's the output:

<h2>posts</h2>

<h2>all</h2>

page: /, title , tags: <br/>
page: /posts/alpha/, title alpha, tags: posts<br/>
page: false, title beta, tags: <br/>

So yeah, my non-draft post was correctly tagged but didn't show up in the collection. I'm not sure what to think about that and possibly it's a bug (after publishing this I'll file - update - I filed it here), but I took another approach.

In .eleventy.js, I added a custom collection:

eleventyConfig.addCollection("blogPosts", function(collectionApi) {
    return collectionApi.getFilteredByTag("posts");
});

And guess what? This too didn't work! It's got to be a timing/chicken+egg/etc type issue. Finally, I switched to a file system solution:

eleventyConfig.addCollection("blogPosts", function(collectionApi) {
    let initial = collectionApi.getFilteredByGlob("posts/*.md");
    return  initial.filter(i => {
        return i.data.tags && i.data.tags === 'posts';
    });
});

Oddly, in this scenario I was able to get my posts, Eleventy properly processed the dynamic tags aspect, and correctly returned a filtered list of non-draft posts. I switched my 'blog post display' logic to:

{% for post in collections.blogPosts %}

Perfect! Note that you want to ensure you consistently use this new collection. So for example, if you have an RSS feed (you do, right?) or a search interface, you'll want to use blogPosts, not posts.

In theory, you can stop reading now, but I had another idea as well.

Iteration Three #

What if you wanted to have 'draft' posts that were published, but not linked to from your list of posts, RSS, and so forth? This would let you publish a draft post and let other folks take a look for review purposes. To be clear, you could just share a link to your repository as well, but if you wanted a non-technical person to do the review, see the post in the context of your website, etc, then publishing, but not including, a draft could be helpful.

We can support this by simply removing the permalink feature:

module.exports = {
    eleventyComputed: {
        tags: data => {
            if(!data.draft) return 'posts';
            return '';
        }
    }
}

Tagging is still done correctly, at least when combined with our custom collection, and beta.md will be written out to the file system. This means you could then share a URL with others to get their feedback on the content.

As a final note, you could also use all of this to support future posts. Ie, don't link/publish if the data is in the future. But you would need to combine that with a scheduled build system to ensure that when it is time for the post to go live, a build is fired off. That could be done with a daily schedule if you weren't concerned about a precise time.

If you would like a copy of the demo code, you may find it here: https://github.com/cfjedimaster/eleventy-demos/tree/master/eleventy_draft_test

Photo by Daniel McCullough on Unsplash