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) 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
- Add Contentful Plugin
- Store Keys Safely
- Get Data from Contentful
- Make an Index
- Make Pages
- Work with Dates (Optional)
Step 1: Add Contentful plugin 🔌
Install the Contentful JavaScript Delivery SDK package:
npm install --save-dev contentful
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
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
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"
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);
};
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
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"
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>
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 setsize
to10
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.)
- The
- Eleventy stores the results in
pagination.items
and I can iterate over them with Nunjucks'sfor
. I store each iteration in a variable namedarticle
- 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 aprevious
ornext
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>
Explanation
- The
pagination
object has made a return, but instead of iterating over 10 items like in the last step, I setsize
to1
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 referencepagination.items[0].fields.title
in my template, but by using an alias, I can referencearticle.fields.title
.
- The
- In my example, I call two fields I've defined in my content model:
title
andmarkdownBody
. 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 callingarticle.fields.title
, I can calltitle
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 thepermalink
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/
- 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
- 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
In your .eleventy.js
configuration file, add this new filter:
const dateFilter = require("nunjucks-date-filter");
module.exports = function (eleventyConfig) {
eleventyConfig.addNunjucksFilter("date", dateFilter);
}
Use it in your templates:
Published {{ published | date('MMM D, YYYY') }}