DEV Community

John Kemp-Cruz
John Kemp-Cruz

Posted on • Originally published at jkc.codes on

Creating Drafts In Eleventy

Several static site generators and content management systems have built in functionality to mark posts as drafts. Eleventy (11ty) isn't one of these but fortunately it is possible to implement.

The core concept is to use two front matter keyspermalink and eleventyExcludeFromCollections — to hide pages from users and then computed data with environment variables to automatically toggle visibility depending on the environment.

What Functionality Is Needed?

The permalink key

The permalink front matter key controls where a file is built to. More practically, it dictates what the URL will be for the page.

When we don't want to create files for drafts or give them a URL, permalink should be set to false. To quote the Eleventy permalink docs: If you set the permalink value to be false, this will disable writing the file to disk in your output folder. The file will still be processed normally (and present in collections, with its url and outputPath properties set to false) but will not be available in your output directory as a standalone template.

We don't want the file to still be present in collections though, that's where eleventyExcludeFromCollections comes in.

The eleventyExcludeFromCollections key

The eleventyExcludeFromCollections front matter key does what it says — it excludes pages from collections. Collections are sets of data from related content that can be used to create dynamic pages such as RSS feeds, site maps or blog post lists.

eleventyExcludeFromCollections should be set to true to hide drafts from lists created from collections. To quote the Eleventy collection docs: In front matter (or further upstream in the data cascade), set the eleventyExcludeFromCollections option to true to opt out of specific pieces of content added to all collections (including collections.all, collections set using tags, or collections added from the Configuration API in your config file). Useful for your RSS feed, sitemap.xml, custom templated .htaccess files, et cetera.

Pages can be hidden on demand by combining permalink and eleventyExcludeFromCollections but what we really want is for them to only be hidden in live production sites so we can continue to test locally. Computed data lets us do that.

Computed Data

Computed data allows modification of front matter values dynamically based on passed in data from the page, front matter or elsewhere. Front matter is usually static but with computed data we can check whether the permalink and eleventyExcludeFromCollections keys need to be toggled based on whether we're working locally or building our site for production.

Environment Variables

Environment variables aren't an Eleventy specific feature so I won't be covering them in any detail but in short they provide a global variable which we can toggle depending on where your code is running.

We will be using the dotenv NPM package because it provides a consistent and easy set up across operating systems.

How To Create Drafts

There are three ways popularised by Jekyll to handle drafts which I'll cover:

  • Using draft keys in front matter
  • Setting a future date
  • Using a draft folder

I'll be focusing on blog posts here but these methods can be used on any type of page as long as there's front matter somewhere in the data cascade.

Set Up Your Environment

The following steps assume that you're building your site on a server using a service like Netlify.

First, install dotenv using npm i dotenv in the command line.

Next, create a .env file in your root directory (the same folder as package.json) and add the following inside the file:

ELEVENTY_ENV=development
Enter fullscreen mode Exit fullscreen mode

This is gives us a global variable "ELEVENTY_ENV" with the value "development" we can import into files later.

Finally, we need to prevent sending the .env file to the server so that it doesn't think it's in a development environment. Do this by adding your .env file to your .gitignore file (create one in your root folder if it doesn't exist):

.env
node_modules
Enter fullscreen mode Exit fullscreen mode

With environment variables set up we can start using them in computed data.

Using Draft Keys Or A Future Date In Front Matter

The goal here is to either:

  • Set a draft key to true or false in the front matter of a page to determine whether it's hidden or not; or
  • Set a date key in the front matter of a page to a future date and only show that page if a build is triggered on or after that date
--------
date: 2150-12-31
draft: true
// Other front matter
--------
// Page content
Enter fullscreen mode Exit fullscreen mode

To do so we'll need to add a directory specific data file which allows us to add the same front matter to all files in a folder:

blog
    |- blog.11tydata.js
    |- first-post.md
    |- second-post.md
    |- third-post.md</pre>
Enter fullscreen mode Exit fullscreen mode

I've used blog.11tydata.js here as it's in the blog folder. If you're using a different folder name replace "blog" in the file name for the folder's name.

Inside of the 11tydata.js file we'll export our front matter data:

require('dotenv').config();

const isDevEnv = process.env.ELEVENTY_ENV === 'development';
const todaysDate = new Date();

function showDraft(data) {
    const isDraft = 'draft' in data && data.draft !== false;
    const isFutureDate = data.page.date > todaysDate;
    return isDevEnv || (!isDraft && !isFutureDate);
}

module.exports = function() {
    return {
        eleventyComputed: {
            eleventyExcludeFromCollections: function(data) {
                if(showDraft(data)) {
                    return data.eleventyExcludeFromCollections;
                }
                else {
                    return true;
                }
            },
            permalink: function(data) {
                if(showDraft(data)) {
                    return data.permalink
                }
                else {
                    return false;
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

On line 1 we're importing dotenv so we can read the environment variables set in our .env file from a process.env object.

On line 3 we're reading the ELEVENTY_ENV environment variable from the process.env object and using it to assign a boolean value to an isDevEnv variable.

On line 4 we're creating a new date object containing today's date.

Lines 6–10 contain the function which will return a boolean determining whether to show the draft page or not.

On line 7 we're checking if a draft key has been set to true in the page's front matter. Note that using const isDraft = data.draft === true; would also work but because of type coercion any typos would assume that the page is not a draft. By explicitly checking for a draft key and that it isn't set to false we can be sure it's meant to be public.

On line 8 we're checking the front matter date against today's date from line 4 to see if it's greater than today's date.

On line 9 we're using the isDevEnv variable from line 3, the isDraft result from line 7 and the isFutureDate variable from line 8 to return a boolean result confirming whether the page should be shown or not.

Lines 12–33 are exporting our front matter data for Eleventy to use in its data cascade. The eleventyComputed key is the only key I'm including here but you can add other front matter keys such as tags in the return object if you need to.

Lines 15–22 and 23–30 are where we're determining the eleventyExcludeFromCollections and permalink values based on the result of the showDraft function being passed data supplied by Eleventy. If the showDraft function from line 6 returns true, we're using the existing values but if it returns false we're overriding the existing values to true and false to exclude the page from collections and prevent the page being built respectively.

Using A Draft Folder

The goal here is to be able to move files in and out of a drafts folder to hide or show them. To do so we'll need to add a directory specific data file to the drafts folder so the same front matter is added to all files in that folder:

blog
    |- first-post.md
    |- second-post.md
drafts
    |- drafts.11tydata.js
    |- third-post.md
Enter fullscreen mode Exit fullscreen mode

I've used drafts.11tydata.js here as it's in the drafts folder. If you're using a different folder name replace "drafts" in the file name for the folder's name.

Inside of the 11tydata.js file we'll export our front matter data:

require('dotenv').config();

const isDevEnv = process.env.ELEVENTY_ENV === 'development';

module.exports = function() {
    return {
        eleventyComputed: {
            eleventyExcludeFromCollections: function(data) {
                if(isDevEnv) {
                    return data.eleventyExcludeFromCollections;
                }
                else {
                    return true;
                }
            },
            permalink: function(data) {
                if(!isDevEnv) {
                    return false;
                }
                else if(data.permalink !== '') {
                    return data.permalink;
                }
                else {
                    return data.page.filePathStem.replace('/drafts/', '/blog/') + '/';
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

On line 1 we're importing dotenv so we can read the environment variables set in our .env file from a process.env object.

On line 3 we're reading the ELEVENTY_ENV environment variable from the process.env object and using it to assign a boolean value to an isDevEnv variable.

Lines 5–29 are exporting our front matter data for Eleventy to use in its data cascade. The eleventyComputed key is the only key I'm including here but you can add other front matter keys such as tags in the return object if you need to.

Lines 8–15 and 16–26 are where we're determining the eleventyExcludeFromCollections and permalink values based on isDevEnv from line 3.

On lines 9–14 we're returning the existing eleventyExcludeFromCollections value if we're in a development environment or true if we're not.

On lines 17–19 we're checking if we're in a development environment and setting the permalink key to false if we're not.

On lines 20–22 we're checking if a permalink has been explicitly set in the front matter and using it if so.

On lines 23–25 we're modifying the page's default permalink so that the "drafts" part of the path is replaced by "blog". Instead of "/drafts/third-post/" we'll output "/blog/third-post/". Note that adding the trailing slash on the end is important.

Summary

The permalink and eleventyExcludeFromCollections front matter keys allow us to create drafts in Eleventy:

--------
permalink: false
eleventyExcludeFromCollections: true
--------
Enter fullscreen mode Exit fullscreen mode

By leveraging computed data and environment variables we can replicate Jekyll's draft behaviour:

Command line:

npm i dotenv
Enter fullscreen mode Exit fullscreen mode

.env:

ELEVENTY_ENV=development
Enter fullscreen mode Exit fullscreen mode

.gitignore:

.env
node_modules
Enter fullscreen mode Exit fullscreen mode

blog.11tydata.js:

require('dotenv').config();

const isDevEnv = process.env.ELEVENTY_ENV === 'development';
const todaysDate = new Date();

function showDraft(data) {
    const isDraft = 'draft' in data && data.draft !== false;
    const isFutureDate = data.page.date > todaysDate;
    return isDevEnv || (!isDraft && !isFutureDate);
}

module.exports = ()=> {
    return {
        eleventyComputed: {
            eleventyExcludeFromCollections: data => showDraft(data) ? data.eleventyExcludeFromCollections : true,
            permalink: data => showDraft(data) ? data.permalink : false,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

drafts.11tydata.js:

require('dotenv').config();

const isDevEnv = process.env.ELEVENTY_ENV === 'development';

module.exports = ()=> {
    return {
        eleventyComputed: {
            eleventyExcludeFromCollections: data => isDevEnv ? data.eleventyExcludeFromCollections : true,
            permalink: data => {
                if(!isDevEnv) { return false; }
                return data.permalink !== '' ? data.permalink : data.page.filePathStem.replace('/drafts/', '/blog/') + '/';
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

post.md:

--------
draft: true
--------
Enter fullscreen mode Exit fullscreen mode

Further Reading

Top comments (0)