Draft Posts in Eleventy

Local-only posts enhance the author experience
Two scholars in a cabinet: Paracelsus (?) and a traditional philosopher examining a flask. Oil painting
Two scholars in a cabinet: Paracelsus (?) and a traditional philosopher examining a flask. Oil painting. Public Domain Mark. Source: Wellcome Collection.

Draft Eleventy posts are included on the front end in development mode only, not on the production site. When using version control software (VCS) the simplest way to make a post dev-only is to not track it— with Git, for example, do not commit the file. But with that approach you miss out on tracking changes to the draft, and you can’t distinguish drafts from published posts on the front end. Another option is a dedicated drafts’ Eleventy collection. But that approach can require reworking plugins to consume both the published posts’ collection and the drafts’ collection (for example when displaying all or related posts); and, with Eleventy’s path-based collections, publishing will require relocating the post file which can complicate looking at its VCS history. A good third option hinges on adding a new “draft” data field to posts.

Contents

Setup

We’ll need a way to distinguish development and production environments. The standard solution is to store an environment-specific variable in an .env file, and to use dotenv to access the env variable in the site JS.

  1. gitignore the path .env. .env files are often used to store sensitive information (read Algolia Search in VuePress Without Joining DocSearch for an example). Here, it will help us distinguish between “dev” and non-“dev” environments.

    .gitignore
    text
    .env
    text
    .env
  2. Create a file .env in your project root, and define DEV to be true.

    .env
    yaml
    yaml
    DEV=true
    yaml
    DEV=true
  3. I like to use an .env.example file to keep a record of all expected environment variables. I leave out sensitive information; “true” isn’t sensitive, so it’s safe to leave in. Then, if you or someone else clones the project, they can cp .env{.example,}.

    .env.example
    yaml
    yaml
    DEV=true
    yaml
    DEV=true
  4. Add the dotenv dependency. Instructions in the dotenv docs. In my case, that’s

    command line
    shell
    pnpm add -D dotenv
    shell
    pnpm add -D dotenv

Local-only Drafts

In production, draft should not be written to the file system (the page should not exist) and they should be excluded from collections (keep them out of post archives, “related post” sections, and anything else that uses a collection).

Setting eleventyExcludeFromCollections: true in a post’s front matter will exclude it from collections.[1] Setting permalink: false will keep it from being written to the file system.[2] So one way of marking a post as a “draft” is

posts/my-post.md
md
md
---
# …
eleventyExcludeFromCollections: true
permalink: false
# …
---
<!-- … -->
md
---
# …
eleventyExcludeFromCollections: true
permalink: false
# …
---
<!-- … -->

But if you set eleventyExcludeFromCollections: true or permalink: false on any posts for a reason other than its being a draft, keeping track of true drafts will be tough.

The solution is to make that data dynamic. Then draft status and publication date can be used as factors in eleventyExcludeFromCollections and permalink.

Use eleventyComputed for dynamic data.[3] Use a collection-level JS data file to define dynamic data for all posts in a collection.[4]

posts/posts.11tydata.js
js
js
require('dotenv').config(); // or ESM: `import 'dotenv/config'`
const dev = process.env.DEV === "true";
const now = new Date();
/**
* If a post is a `draft`, it is for dev mode only.
*
* @param {object} data Post data
* @param {boolean} [data.draft=false] Post draft status
* @returns {boolean}
*/
function devOnly(data) {
return Boolean(data.draft);
}
module.exports = {
eleventyComputed: {
eleventyExcludeFromCollections: data => {
if (!dev && devOnly(data)) {
return true;
}
return data.eleventyExcludeFromCollections;
},
permalink: data => {
if (!dev && devOnly(data)) {
return false;
}
return data.permalink;
},
},
// …
}
js
require('dotenv').config(); // or ESM: `import 'dotenv/config'`
const dev = process.env.DEV === "true";
const now = new Date();
/**
* If a post is a `draft`, it is for dev mode only.
*
* @param {object} data Post data
* @param {boolean} [data.draft=false] Post draft status
* @returns {boolean}
*/
function devOnly(data) {
return Boolean(data.draft);
}
module.exports = {
eleventyComputed: {
eleventyExcludeFromCollections: data => {
if (!dev && devOnly(data)) {
return true;
}
return data.eleventyExcludeFromCollections;
},
permalink: data => {
if (!dev && devOnly(data)) {
return false;
}
return data.permalink;
},
},
// …
}

Now, setting draft: true in a post’s front matter will make it a draft, and it will not be part of production builds.

posts/my-post.md
md
md
---
title: This is a draft
draft: true
---
<!-- … -->
md
---
title: This is a draft
draft: true
---
<!-- … -->

Group drafts in collection post lists

With a bunch of drafts in the hopper, “all posts” lists can be cluttered and significantly different in development from in production. Consider grouping drafts together. The Eleventy docs suggest creating a new collection with a custom sort[5]; I prefer a custom Eleventy filter.

.eleventy.js
js
js
module.exports = function(eleventyConfig) {
// …
eleventyConfig.addFilter("collectionOrder", (posts) => {
const drafts = [];
const published = [];
for (const post of posts) {
if (post?.data?.draft) {
drafts.push(post);
continue;
}
published.push(post);
}
return [...drafts, ...published];
});
// …
}
js
module.exports = function(eleventyConfig) {
// …
eleventyConfig.addFilter("collectionOrder", (posts) => {
const drafts = [];
const published = [];
for (const post of posts) {
if (post?.data?.draft) {
drafts.push(post);
continue;
}
published.push(post);
}
return [...drafts, ...published];
});
// …
}
nunjucks
twig
<ul>
{% for post in collections.posts %}
{% for post in collections.posts|collectionOrder %}
<li>
{{ post.title }}
</li>
{% endfor %}
</ul>
twig
<ul>
{% for post in collections.posts %}
{% for post in collections.posts|collectionOrder %}
<li>
{{ post.title }}
</li>
{% endfor %}
</ul>

Mark drafts in the UI

Posts’ draft data can be used to surface draft status on the front end:

nunjucks
twig
<ul>
{% for post in collections.posts|collectionOrder %}
<li>
{{ post.data.title }}
{% "(draft)" if post.data.draft and not post.data.draft == "false" %}
</li>
{% endfor %}
</ul>
twig
<ul>
{% for post in collections.posts|collectionOrder %}
<li>
{{ post.data.title }}
{% "(draft)" if post.data.draft and not post.data.draft == "false" %}
</li>
{% endfor %}
</ul>

Footnotes

  1. https://www.11ty.dev/docs/collections/#how-to-exclude-content-from-collections ↩︎

  2. https://www.11ty.dev/docs/permalinks/#skip-writing-to-the-file-system ↩︎

  3. https://www.11ty.dev/docs/data-computed/ ↩︎

  4. https://www.11ty.dev/docs/data-js/ ↩︎

  5. https://www.11ty.dev/docs/collections/#advanced-custom-filtering-and-sorting ↩︎

Articles You Might Enjoy

Or Go To All Articles