Creating Drafts In Eleventy
Tags: 11ty, eleventy, webdev
Published: 2021-08-17 16:06:50

---
title: Creating Drafts In Eleventy
published: true
date: 2021-08-06 13:39:37 UTC
tags: [11ty, eleventy, webdev]
canonical_url: https://jkc.codes/blog/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](https://www.11ty.dev/docs/data-frontmatter/) [keys](https://www.11ty.dev/docs/data-configuration/) — [`permalink`](https://www.11ty.dev/docs/permalinks/) and [`eleventyExcludeFromCollections`](https://www.11ty.dev/docs/collections/#option-exclude-content-from-collections) — to hide pages from users and then [computed data](https://www.11ty.dev/docs/data-computed/) 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](https://www.11ty.dev/docs/permalinks/#permalink-false): <q cite="https://www.11ty.dev/docs/permalinks/#permalink-false">If you set the <code>permalink</code> value to be <code>false</code>, 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 <a href="https://www.11ty.dev/docs/data-eleventy-supplied/">url and outputPath properties</a> set to <code>false</code>) but will not be available in your output directory as a standalone template.</q>

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](https://www.11ty.dev/docs/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](https://www.11ty.dev/docs/collections/#option-exclude-content-from-collections): <q cite="https://www.11ty.dev/docs/collections/#option-exclude-content-from-collections">In front matter (or further upstream in the data cascade), set the <code>eleventyExcludeFromCollections</code> option to <code>true</code> to opt out of specific pieces of content added to all collections (including <code>collections.all</code>, collections set using tags, or collections added from the Configuration API in your config file). Useful for your RSS feed, <code>sitemap.xml</code>, custom templated <code>.htaccess</code> files, et cetera.</q>

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](https://www.11ty.dev/docs/data-computed/) 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](https://www.npmjs.com/package/dotenv) 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](https://www.11ty.dev/docs/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:

```text
ELEVENTY_ENV=development
```

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):

```text
.env
node_modules
```

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](https://www.11ty.dev/docs/dates/) 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

```yaml
---
date: 2150-12-31
draft: true
// Other front matter
---
// Page content
```

To do so we'll need to add a [directory specific data file](https://www.11ty.dev/docs/data-template-dir/) which allows us to add the same front matter to all files in a folder:

```text
blog
	|- blog.11tydata.js
	|- first-post.md
	|- second-post.md
	|- third-post.md</pre>
```

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:

```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 = 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;
				}
			}
		}
	}
}
```

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](https://www.11ty.dev/docs/data-eleventy-supplied/). 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](https://www.11ty.dev/docs/data-template-dir/) to the drafts folder so the same front matter is added to all files in that folder:

```text
blog
	|- first-post.md
	|- second-post.md
drafts
	|- drafts.11tydata.js
	|- third-post.md
```

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:

```js
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/') + '/';
				}
			}
		}
	}
}
```

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](https://www.11ty.dev/docs/permalinks/#remapping-output-(permalink)).

## Summary

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

```yaml
---
permalink: false
eleventyExcludeFromCollections: true
---
```

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

Command line:
```shell
npm i dotenv
```

.env:
```text
ELEVENTY_ENV=development
```

.gitignore:
```text
.env
node_modules
```

blog.11tydata.js:
```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,
		}
	}
}
```

drafts.11tydata.js:
```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/') + '/';
			}
		}
	}
}
```

post.md:
```yaml
---
draft: true
---
```

## Further Reading

- [Permalinks documentation](https://www.11ty.dev/docs/permalinks/)
- [Collections documentation](https://www.11ty.dev/docs/collections/)
- [Computed data documentation](https://www.11ty.dev/docs/data-computed/)
- [Data cascade documentation](https://www.11ty.dev/docs/data-cascade/)
- [Directory specific data files documentation](https://www.11ty.dev/docs/data-template-dir/)