Creating a content calendar for your Contentful content

Con whitepaper 2 p2r2 1 2

Content calendars are an essential part of any content management toolkit. They display your content in a familiar calendar view, making it easy to see when content is being published.

Scheduling is also a breeze. Want to change when an item is published? Simply drag the item to the new date and you’re done.

Final product of the content calendar

A content calendar is a publisher-specific view of content. Traditionally, such a view would be part of the content management app. But with Contentful we don’t have to follow tradition. Rather than creating content management Swiss Army knives, we can create streamlined, uncluttered, fit-for-purpose interfaces.

In this article, we’ll step through how to create a content calendar of our Contentful content. We’ll create a focussed single-page app that uses the RiotJS library and Bulma CSS framework to create the interface. It makes use of the Contentful Content Management API Javascript SDK to both read and update our content.

We’ll be building it for a single content type called article.

Catering for future publishing

Content calendars provide future publishing views of your content. They allow you to say: I want to publish this content on this date.

Out-of-the-box, Contentful doesn’t support future publishing. So, we will need to create a new property, publishDate, to hold our future publishing date. When that date becomes due, we can then publish the item, either manually or via a triggered process.

Adding the new publishDate property

  1. Go to the Contentful app
  2. Select Content model in the top menu
  3. Select the content type you wish to display in the calendar (in this case article)
  4. Click on Add field
  5. Select Date and time
  6. Enter Publish Date in the Name field on the pop-up dialog
  7. Click Create

Setting a publish date

With our publishDate property in place, we can now start building the calendar.

Creating our calendar app

We’re going to use RiotJS to build the interface. I like Riot’s ‘simple and elegant component’ approach. Of course, you might prefer React, Angula, Ember, etc. But the interactions between the web app and Contentful will be largely the same.

Our interface consists of two main components:

  1. Pipeline - lists all draft articles that are yet to be assigned a future publishing date
  2. Calendar - standard calendar view containing articles that have been assigned a publishing date

All draft articles can be dragged between the pipeline and the calendar, where:

  • dragging an article to the pipeline removes the publishDate, and
  • dragging to a future date only sets the publishDate.

We’ll include published articles in the calendar so that we can see what’s been published recently. However, published articles will not be be draggable.

The pipeline and calendar components are wrapped in an app component just to keep things nice and tidy. It also houses our drag functionality. More on that later.

Status in Contentful

It’s going to be useful to understand how status is worked out in Contentful. If you look at your content in the Contentful app, you’ll see Draft, Published, Updated and Archived.

Each entry in Contentful has a set of system properties and a set of user-defined properties, the fields that you define in the content model. Contentful does not use a dedicated status property but determines an entry’s status by checking a number of system properties:

  • If the entry has no publishedAt date (or publishedVersion) then it’s in Draft
  • If the entry has a publishedAt date and version is the same as publishedVersion then it’s Published
  • If the entry has a publishedAt date and version is not the same as publishedVersion then it’s Updated. Note, the entry is still published, it’s just that the latest updates have not been published
  • If the entry has an archivedDate then it is Archived.

We need to keep this in mind when getting the data for our components.

The Pipeline

The pipeline component lists Draft articles that have not yet been assigned a future publishing date.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<content-pipeline>

  <div id="pipeline">
    <header class="level" style="margin-bottom:0">
      <div class="level-left">
        <span class="level-item is-size-4">Pipeline</span>
      </div>
    </header>
    <div class="pipeline-articles">
      <content-entry each={items} item={this} id={sys.id}></content-entry>
    </div>
  </div>


  <style scoped>

    #pipeline {
      border-right: 1px solid #eee;
    }

    #pipeline > header {
      border-bottom: 1px solid #eee;
      padding: 1rem 2rem;
    }

    .pipeline-articles {
      overflow-y: auto;
      padding: 0rem 1rem 0.25rem 1rem;
    }

  </style>


  <script>

    var self = this;
    self.items = [];

    // when component created, set the height and get the data
    self.on('mount',function(){
      setHeight();
      bus.trigger('load_pipeline');
    });

    // when resize event triggered, set the component’s height
    bus.on('resize',setHeight);

    // when pipeline loaded event is triggered, update the component with the new data
    bus.on('pipeline_loaded', function(items){
      self.items = items;
      self.update();
    });

    function setHeight(){
      let p = document.querySelector('.pipeline-articles');
      let b = p.getBoundingClientRect();
      p.style.height = (window.innerHeight - b.top - 5).toString() + 'px';
    }

  </script>

</content-pipeline>

This is a pretty typical of a Riot component: - A template used to generate the HTML output - Localised CSS style statements - Script for localised logic and functionality

In the template, we’re actually calling another component to output the pipeline articles.

1
<content-entry each={items} item={this} id={sys.id}></content-entry>

This allows us to reuse the same layout for both pipeline and calendar items. items is a JSON array of our pipeline articles. The each attribute is a loop function: Riot will output a content-entry component for each member of the items array.

The content entry

It’s worth taking a quick look at the content entry component. It has no script but does add some vital attributes to the item:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<content-entry>

  <article class="card {published: opts.item.sys.publishedAt}" id={opts.id}>
    <header>
      <p class="is-size-6">
        {opts.item.fields.title['en-US']}
      </p>
    </header>
  </article>

  <style scoped>
	...
  </style>

</content-entry>

It’s just outputting a simple Bulma card that only contains the article’s title. Importantly, though, it adds a published class if the article has a sys.publishedAt property. We’ll be using this later when implementing the drag-and-drop update of the publishDate property.

Getting the data into the pipeline

We’re taking advantage of Riot’s event-driven functionality to get the pipeline data into our app. Here’s how it works.

Getting the data into the pipeline

If you look in app.js you’ll find the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
bus.on('load_pipeline', function(){

  // make a raw request to get the pipeline entries - no publishDate, no publishedAt
  client.rawRequest({
    method: 'GET',
    url : CONFIG.space_id + '/entries/?&content_type=' + CONFIG.content_type + '&sys.publishedAt[exists]=false&fields.publishDate[exists]=false&order=fields.title'
  })
  .then(function(data){
    bus.trigger('pipeline_loaded', data.items);
  })
  .catch(console.error);

});

This function: 1. Traps the load_pipeline event raised when our pipeline component is first built. 2. Uses the Contentful Management Javascript SDK to grab articles from our Contentful space that don’t have both a publishedAt property (i.e. are Draft) and a publishDate property. 3. Triggers the pipeline_loaded event, passing the articles returned by Contentful.

We use the rawRequest method of the SDK as we can use the returned JSON directly when updating our pipeline output.

The pipeline component traps the pipeline_loaded event and uses the passed articles to update itself:

1
2
3
4
    bus.on('pipeline_loaded', function(items){
      self.items = items;
      self.update();
    });

This event-driven approach allows us to keep our components relatively simple and easier to reuse.

The Calendar

The calendar uses the same event-driven approach to build and load the calendar data.

1
2
3
4
bus.on('calendar_loaded', function(items){
    self.calendar = buildCalendar(items);
    self.update();
});

The buildCalendar function creates a JSON representation of a calendar (months and days). It then appends articles with a defined publishDate property, or that have already been published, to the appropriate day.

The result is the following JSON structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
	weeks: [
		days: [
			{
				iso: …, /* ISO formatted date */,
				local: …, /* date in local format */,
				day: [1-31], /* day of month */
				month_name: “JAN”, /* abbreviated Month name */
				entries: [], /* array of items where publishDate or publishedAt equals this day */ 
			},
		]
] 
}

Obviously, we have a maximum of 7 items in the days array. The number of weeks depends on the range we are displaying. The default is 4 weeks prior to the current date and 12 weeks after the current date.

The calendar data is then used to update the component, using the following template:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  <div id="calendar">
    <table>
      <thead>
        <tr>
          <th width="14.25%">Sunday</th>
          <th width="14.25%">Monday</th>
          <th width="14.25%">Tuesday</th>
          <th width="14.25%">Wednesday</th>
          <th width="14.25%">Thursday</th>
          <th width="14.25%">Friday</th>
          <th width="14.25%">Saturday</th>
        </tr>
      </thead>
    </table>
    <div id="calendar-days">
      <table>
        <tbody>
          <tr each={calendar.weeks} class="week">
            <td width="14.25%" each={day, index in days} class="day {current:day.current==1, today: day.today==1, firstday: day.day==1}" data-date-iso={day.iso} data-date-local={day.local}>
              <span class="daynum"><span if={day.day==1||index==0}>{day.month_name}&nbsp;</span>{day.day}</span>
              <content-entry each={day.entries} item={this} id={sys.id}></content-entry>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>

The output is split into two tables: one for headings and one for the actual days. This allows the days to be scrolled whilst keeping the headings stationary.

The template creates a table row for each week and a table cell for each day. Each table cell will contain any articles that are being published on that day. The data attributes data-date-iso and data-date-local are added to help with updating the publishDate when an article is dragged to a new position.

Enabling drag-and-drop changing of the publish date

The publishDate can be updated by editing the article in the Contentful app. But it would be much easier to just drag it to a new day in the calendar.

To do that, we’ll use the excellent Dragula javascript library. But first let’s consider what rules we need.

  1. Published articles cannot be dragged (has a class of published)
  2. Articles cannot be dropped on past dates (target must have a class of current)
  3. Articles can be dropped in the pipeline - this removes the publishDate (target has a class of pipeline-items)

Now let’s translate these into Dragula:

1
2
    // set up drag
    let drake = dragula({

Our calendar table’s cells have a day class. Our pipeline articles are wrapped in a div with the pipeline-items class. So, let’s restrict drag-and-drop to articles that are children of these containers:

1
2
3
      isContainer: function (el) {
        return el.classList.contains('day') || el.classList.contains('pipeline-items');
      },

Now let’s narrow down where articles can be dropped. The target must have a current class (calendar table cells for today onwards) or a pipeline-items class:

1
2
3
      accepts: function (el, target, source, sibling) {
        return target.classList.contains('current') || target.classList.contains('pipeline-items');
      },

And finally lets prevent articles with a published class from being selected at all:

1
2
3
      invalid: function (el, handle) {
        return el.classList.contains('published');
      }

Now let’s handle the dropping of an articles. Let’s start by checking if we actually need to do anything:

1
2
3
    drake.on('drop', function(el,target,source,sibling){

	if (target===source) return;

When an article is dropped onto a future day, let’s update the publishDate. The article’s id is on the element being dropped (el). The new publishDate value is stored as a data attribute on the table cell (target):

1
2
3
4
5
6
      if (target.classList.contains('current')){
        // moving to a day - change publish date
        let entry_id = el.getAttribute('id')
        let publish_date = target.getAttribute('data-date-iso')
        bus.trigger('set_publish_date',{id:entry_id, publish_date:publish_date});
      }

And when an article is dropped back into the pipeline then let’s clear its publishDate:

1
2
3
4
5
6
7
     if (target.classList.contains(pipeline-items)){
        // clear publish date
        let entry_id = el.getAttribute('id');
        bus.trigger('set_publish_date',{id:entry_id, publish_date:null});
      }

    });

In both cases, we trigger a set_publish_date event. This is trapped by our app.js which uses the Javascript SDK to update the article:

1
bus.on('set_publish_date',function(data){

First we get a reference to our Contentful space and environment:

1
2
3
4
  client.getSpace(CONFIG.space_id)
    .then(function(space){
      return space.getEnvironment(CONFIG.environment_id);
    })

Now we can get the article using the passed id:

1
2
3
4
5
    .then(function(entry){
      let publish_date = {'en-US': data.publish_date}
      entry.fields.publishDate = publish_date;
      return entry.update();
    })

We’ve got the article so let’s update the publishDate:

1
2
3
4
5
6
    .then(function(entry){
      console.log('Entry ' + entry.sys.id + ' updated.');
    })
    .catch(console.error);

});

Let’s log the update or any error that’s occured:

1
2
3
4
5
6
    .then(function(entry){
      console.log('Entry ' + entry.sys.id + ' updated.');
    })
    .catch(console.error);

});

Authenticating with the Content Management API

The calendar needs to include an API token when making requests to the Content Management API. There are a couple of different ways of getting a token, depending on how you want to use your app.

Personal Access Tokens

You can create Personal Access Tokens in the Contentful app. They effectively give all users of the app the same permissions as you.

To create a Personal Access Token:

  1. Open the Contentful app
  2. Select Space Settings in the top menu
  3. Select API Keys in the drop-down list
  4. In the API screen, select the Content management tokens tab
  5. Click on Generate personal token
  6. In the pop-up dialog give your token a name, click on Generate

Your token will be generated and displayed in the response. Copy it into your config file.

Remember this token will be in your client-side code. You should not use it in publicly available apps.

OAuth tokens

The other option is to use OAuth tokens. These grant the same permissions to your app as the Contentful user. When the user opens your application the following steps takes place:

  1. App checks if a token exists
  2. If none is found the user is redirected to the Contentful OAuth endpoint
  3. User logs into Contentful and grants permissions
  4. Contentful redirects back to the app with a token in url
  5. App retrieves the token and stores it for use with Content Management API

Your app can persist the local storage of the token to prevent having to go through the process every time the app is opened.

Before you can use OAuth tokens you need to register your application with Contentful.

register your application with Contentful

When you’ve registered your app, update config.js with the Client ID and the Redirect URI.

If you don’t specify a Personal Access Token in the config.js file then the calendar app will use OAuth tokens.

Publishing content

A content calendar is about future publishing. To really complete the process we want to automate the publishing of articles based on their publishDate property.

This can be done by a regularly scheduled cloud function that publishes articles that have an appropriate publishDate.

Here’s an example in Python that uses the Contentful Management SDK:

Import the relevant libraries

1
2
from contentful_management import Client
from datetime import datetime, timedelta

Set some constants: master is the default environment id.

1
2
3
4
5
TOKEN = 'add your-CMA-token here'
SPACE_ID = 'add your-space-id here'
ENVIRONMENT_ID = 'master'
FREQUENCY = 24 # hours between runs of this function
CONTENT_TYPE = 'article' # which content type are we working with

Create the Contentful Management client

1
client = Client(TOKEN)

Get the entries with a publishDate that is between now and the last time function ran.

1
2
3
4
5
6
7
8
9
10
11
12
13
def getEntries():

    now = datetime.utcnow() # publish dates are stored in UTC
    last = now - timedelta(hours=FREQUENCY)
    comp_from = last.strftime('%Y-%m-%dT%H:%m:%s')
    comp_to = now.strftime('%Y-%m-%dT%H:%m:%s')
    print('Getting entries with publish_date between {} and {}'.format(comp_from,comp_to))

    return client.entries(SPACE_ID,ENVIRONMENT_ID).all({
        'content_type' : CONTENT_TYPE,
        'fields.publishDate[gte]' : comp_from,
        'fields.publishDate[lte]' : comp_to
    })

Loop through the returned articles and publish if necessary.

1
2
3
4
5
6
7
8
9
10
def publishEntries():

    entries = getEntries()
    print('Found {} entries...'.format(len(entries)))
    for entry in entries:
        if entry.is_published:
            print('{} already published'.format(entry.title))
        else:
            entry.publish()
            print('Published {}'.format(entry.title))

This is a very basic approach. You might want to add checking that linked entries and assets are also published and either reporting exceptions or publishing them too.

The benefit of fit-for-purpose applications

The content calendar is a great example of an app that provides limited functionality to a specific audience.

Traditional content management systems generally achieve this via plugins. This increases the complexity of the interface, introduces potential conflicts with other plugins and can negatively impact upgrade paths.

Contentful, with its excellent API support, allows us to quickly and easily build small, agile standalone apps. They are easier to learn, easier to use and easier to maintain.

The content manager’s toolkit is no longer a swag of competing plugins. But a suite of complementary, fit-for-purpose apps.

Updates in your inbox

Subscribe to receive the most important updates. We send eNewsletters once a month that cover trending topics and feature updates.