Static Site, Meet CMS: Using Contentful with 11ty
Tags: cms, blog, contentful, 11ty
Published: 2020-11-30 16:46:47

---
canonical_url: https://www.ianjmacintosh.com/articles/getting-content-from-contentful-into-eleventy/
---
I wanted my personal site to pull content from a CMS instead of having to manage a bunch of content files in my project's filesystem. I found a really excellent article (_[Integrating Contentful with Eleventy to create static sites](https://www.contentful.com/blog/2020/07/28/integrating-contentful-with-eleventy-create-static-sites/)_) on Contentful's website explaining how to do this, but wanted to share my own experience and an abbreviated version of the steps.

## Prerequisites

Since this article is about getting content from Contentful into Eleventy, I presume you have an Eleventy project running and a Contentful space with content in it.

## Steps

1. Add Contentful Plugin
2. Store Keys Safely 
3. Get Data from Contentful
4. Make an Index
5. Make Pages
6. Work with Dates (Optional)

---

### Step 1: Add Contentful plugin 🔌

Install the [Contentful JavaScript Delivery SDK](https://www.npmjs.com/package/contentful) package:

```bash
npm install --save-dev contentful
```

---

### Step 2: Store keys safely 🔑

Find your _Space ID_ and _API access tokens_ in the [Contentful admin panel](https://app.contentful.com/), under _Settings_ > _API keys_.

You need to store these secret values somewhere the Contentful JavaScript Delivery SDK can find them to authenticate itself with Contentful. I like to store them as environment variables in a local `.env` file and use _[dotenv](https://www.npmjs.com/package/dotenv)_ to manage them. You don't need to do it this way, it's just the simplest way I know.

Install the [dotenv](https://www.npmjs.com/package/dotenv) package:

```bash
npm install --save-dev dotenv
```

If you're using git for source control, update your `.gitignore` file to ignore the `.env` file you're about to create. This is important so you won't store sensitive info in your repo.

##### Example `.gitignore`:
```bash
node_modules/
_site/
.env
```

Now you're ready to make your `.env` file. Here's a starter template, replace the placeholder values with your space ID and access token. Again, these can be found in the [Contentful admin panel](https://app.contentful.com/), under _Settings_ > _API keys_.

##### Example `.env`:
```bash
 # Content Delivery API host:
 CTFL_HOST="cdn.contentful.com"

 # Contentful Space ID:
 CTFL_SPACE="space_id_placeholder"

 # Content Delivery API access token:
 CTFL_ACCESSTOKEN="content_delivery_api_access_token_placeholder"
```

This `.env` setup is what I use on my local dev environment, but if you're hosting your site with a third party, you may prefer to use their control panel for managing environment variables.

---

### Step 3: Get Data from Contentful 🚅

Add a new data source by adding a new file to your `/_data` directory. I named mine `contentful.js` because that's what [the tutorial](https://www.contentful.com/blog/2020/07/28/integrating-contentful-with-eleventy-create-static-sites/) did.

##### Example `/_data/contentful.js`:
```js
require("dotenv").config();

const contentful = require("contentful");
const client = contentful.createClient({
  host: process.env.CTFL_HOST,
  space: process.env.CTFL_SPACE,
  accessToken: process.env.CTFL_ACCESSTOKEN,
});

module.exports = function () {
  return client
    .getEntries({
      content_type: "article",
    })
    .then(function (response) {
      const items = response.items.map(function (item) {
        console.log("🍾");
        return item;
      });

      console.log(`Got ${items.length} items from Contentful`);
      return items;
    })
    .catch(console.error);
};
```

See the `getEntries()` call? The `content_type` property tells Contentful what data you want. Replace `article` with `post` or whatever your _content type ID_ is. If you're not sure, check in the [Contentful admin panel](https://app.contentful.com/), under _Content model_ and select your desired content type. When I wrote this, it was on the right side of the page.

If you haven't already done so, start your Eleventy server:
```bash
eleventy --serve
```

If the data call works, your console window running your server should show some champagne bottles and "Got 4 items from Contentful" in your console (or however many __published__ items are available). If you have no published items, you'll get 0 champagne bottles and might want to switch to the Content Preview API for testing purposes. See below for more on that.

Once you confirm things work, you can remove the `console.log` statements and begin the next step, working with this data to build an index.

#### Planning on Working with Unpublished Content? 👀

You can get unpublished content from Contentful by calling the [Content Preview API](https://www.contentful.com/developers/docs/references/content-preview-api) instead of the [Content Delivery API](https://www.contentful.com/developers/docs/references/content-delivery-api/) (which only exposes published content).

If you'd like to use the Content Preview API, switch your `CTFL_HOST` value to `preview.contentful.com` and your `CTFL_ACCESSTOKEN` to your Content Preview API access token. You can find this with the other values, in the [Contentful admin panel](https://app.contentful.com/), under _Settings_ > _API keys_. I add duplicate lines for in my `.env` file, and when I want to switch between Content Preview API and Content Delivery API, I comment/uncomment the relevant lines.

##### Example `.env` for working with unpublished content:
```bash
# Contentful "master" Space ID:
CTFL_SPACE="abc123def456"

# Content Delivery API host:
# CTFL_HOST="cdn.contentful.com"

# Content Delivery API access token:
# CTFL_ACCESSTOKEN="abcdefghijklmnopqrstuvwxyz1234567890"

# Content Preview API host:
CTFL_HOST="preview.contentful.com"

# Content Preview API access token:
CTFL_ACCESSTOKEN="0987654321abcdefghijklmnopqrstuvwxyz"
```

---

### Step 4: Make an Index 📖

You can build a list of pages from your data using Eleventy's [_pagination_](https://www.11ty.dev/docs/pagination/) feature. I use [Nunjucks](https://mozilla.github.io/nunjucks/) for a templating language in this example to build logic into my templates.

My example will only work with 2 or more items in the data; the `for` statement in Nunjucks won't work with only one item. If you only have one item, you may want to jump to the next step.

##### Example `/all-pages.njk`:
```twig
---
pagination:
  data: contentful
  size: 10
---
<!DOCTYPE html>
<html lang="en">
<body>
<ul>
{% for article in pagination.items %}
<li>
    <a href="/{{ article.fields.title | slug }}/">
        {{ article.fields.title }}
    </a>
</li>
{% endfor %}
</ul>
{% if pagination.href.previous %}
<p>
    <a href="{{ pagination.href.previous }}">Previous</a>
</p>
{% endif %}
{% if pagination.href.next %}
<p>
    <a href="{{ pagination.href.next }}">Next</a>
</p>
{% endif %}
</body>
</html>
```

#### Explanation
* The `pagination` object tells Eleventy how to work with the data set:
  * The `data` property is a reference to my data source (in my case, `contentful` for `/_data/contentful.js`)
  * The `size` property is how many items I want on each page. Eleventy will make as many pages as necessary to show all the data. For example, if I set `size` to `10` and have 47 items, Eleventy will make five pages; 10 items on each of the first four pages, and 7 items on the last page. Their URLs will be generated automatically by Eleventy (`/all-pages/index.html`, `/all-pages/1/index.html`, `/all-pages/2/index.html`, etc.)
* Eleventy stores the results in `pagination.items` and I can iterate over them with Nunjucks's `for`. I store each iteration in a variable named `article`
* I link to each item using each article's "title" field and Eleventy's built-in [`slug` filter](https://www.11ty.dev/docs/filters/slug/). This filter lets me link to `/getting-content-from-contentful-into-eleventy/` instead of `/Getting Content from Contentful into Eleventy/`)
  * ✋🏻 __These pages don't exist yet! If I click a link, I get a 404, and that's fine for now.__
* The `pagination.href` object will have a `previous` or `next` property if there's a previous or next page, and its value will be its URL.

Once you navigate to `/all-pages/` (or whatever you named yours) and confirm your list of all pages shows broken links for your content, you're ready to make the pages themselves.

---

### Step 5: Make Pages 📄

I use pagination again to build the pages themselves, specifying a `size` of `1` to put one item on each page.

##### Example `/page.njk`:
```twig
---
pagination:
  data: contentful
  size: 1
  alias: article

templateEngineOverride: njk,md
permalink: "/{{ article.fields.title | slug }}/"
eleventyComputed:
  title: "{{ article.fields.title }}"
  content: "{{ article.fields.markdownBody }}"
---
<!DOCTYPE html>
<html lang="en">
<body>
<h1>{{ title }}</h1>
    {{ content }}
</body>
</html>
```

#### Explanation
* The `pagination` object has made a return, but instead of iterating over 10 items like in the last step, I set `size` to `1` item and use that item by itself for this page.
  * The `alias` property defines how I can refer to the items coming from Contentful. Without an alias, I could reference `pagination.items[0].fields.title` in my template, but by using an alias, I can reference `article.fields.title`.
* In my example, I call two fields I've defined in my content model: `title` and `markdownBody`. Review your own content model to locate the keys for content you'd like to display.
* The `eleventyComputed` object allows me to reference values even more easily in my template. Instead of calling `article.fields.title`, I can call `title` to get the same data.
* The [`templateEngineOverride` property](https://www.11ty.dev/docs/languages/#overriding-the-template-language) lets me force specific templating engine parsing rules. I use Nunjucks ("njk") for templating with Markdown ("md") content in my data. Eleventy automatically parses `.njk` files using Nunjucks, but since I _also_ want Markdown (to compile my Markdown into HTML), I manually specify both: `njk,md`
* The `permalink` property tells Eleventy where to save each page corresponding with each item. You can use template strings to give each page a unique name. If you omit the `permalink` property, Eleventy's default behavior is to add a numbered page for each item. (e.g., `/page/1/index.html`)
  * __When my permalink value didn't end with a trailing slash, I got weird errors trying to load each page. My browser tried to save the page instead of viewing it in the browser, and then I got `ENOTDIR: not a directory` errors until I manually deleted `_site/`__
* If you already have a layout template you'd like to use, you can call it in your front matter: `layout: _template.njk`

To test, go to your console and confirm a new page is generated for each item retrieved from Contentful. (e.g., `Writing _site/getting-content-from-contentful-into-eleventy/index.html from ./page.njk.`) Navigate to that URL (`/getting-content-from-contentful-into-eleventy/index.html`) in your web browser.

That's it, you're done! 🏁

Due to the static nature of Eleventy, you'll need to generate your site again to see changes after modifying your content in Contentful.

---

### Optional: Use Moment to Work with Dates 🗓
To show _Published on Dec 25, 2018_ on my pages, I use a [Moment plugin for Nunjucks](https://www.npmjs.com/package/nunjucks-date-filter). Since Contentful stores dates in "2020-09-03T03:53:54.665Z" format, I wanted to show that same information in a way that's a little easier on the eyes. I've had nothing but good experiences with Moment even though it's probably overkill for this use case.

Install the [Nunjucks date filter](https://www.npmjs.com/package/nunjucks-date-filter) package:
```bash
npm install --save-dev nunjucks-date-filter
```

In your `.eleventy.js` configuration file, add this new filter:

```js
const dateFilter = require("nunjucks-date-filter");

module.exports = function (eleventyConfig) {
  eleventyConfig.addNunjucksFilter("date", dateFilter);
}
```

Use it in your templates:

```twig
Published {{ published | date('MMM D, YYYY') }}
```