Static Site, Meet CMS: Using Contentful with 11ty

ianjmacintosh

Ian MacIntosh

Posted on November 30, 2020

Static Site, Meet CMS: Using Contentful with 11ty

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

npm install --save-dev contentful
Enter fullscreen mode Exit fullscreen mode

Step 2: Store keys safely 🔑

Find your Space ID and API access tokens in the Contentful admin panel, 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 to manage them. You don't need to do it this way, it's just the simplest way I know.

Install the dotenv package:

npm install --save-dev dotenv
Enter fullscreen mode Exit fullscreen mode

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:
node_modules/
_site/
.env
Enter fullscreen mode Exit fullscreen mode

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, under Settings > API keys.

Example .env:
 # 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"
Enter fullscreen mode Exit fullscreen mode

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 did.

Example /_data/contentful.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);
};
Enter fullscreen mode Exit fullscreen mode

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

eleventy --serve
Enter fullscreen mode Exit fullscreen mode

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 instead of the 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, 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:
# 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"
Enter fullscreen mode Exit fullscreen mode

Step 4: Make an Index 📖

You can build a list of pages from your data using Eleventy's pagination feature. I use 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:
---
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>
Enter fullscreen mode Exit fullscreen mode

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. 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:
---
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>
Enter fullscreen mode Exit fullscreen mode

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 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. 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 package:

npm install --save-dev nunjucks-date-filter
Enter fullscreen mode Exit fullscreen mode

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

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

module.exports = function (eleventyConfig) {
  eleventyConfig.addNunjucksFilter("date", dateFilter);
}
Enter fullscreen mode Exit fullscreen mode

Use it in your templates:

Published {{ published | date('MMM D, YYYY') }}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
ianjmacintosh
Ian MacIntosh

Posted on November 30, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related