To extend or not extend: understanding Contentful UI Extensions

A quick dive into the UI Extensions SDK from a new-to-Contentful point of view



This article is aimed at people planning to work with Contentful UI Extensions, but who have little prior knowledge of the extensions. We’ll try to give a quick overview of the existing extensions, and the pitfalls we have encountered without going into all the details.

Project introduction

We had a project for testing the possibilities (and limitations) of Contentful, one of the leading headless CMS products.

These were the basic requirements for the project:

  • create and edit an article within Contentful in the default language, called a ‘master article’
  • external users from several different teams approve the master article (teams also consist of roles i.e. legal, product team, marketing, etc)
  • export the master article for handover to translation teams and import translated versions into Contentful
  • article provides enough flexibility to the editor concerning structure and nesting of topics
  • editor starts the approval workflow for the master
  • editor starts the translation process and downloads the master file
  • editor always sees the current state of the running approvals

Why implement ex- and import for translation when Contentful provides several options for localization?

The answer is simple: user accounts and the powers of current translation software. For a global client, you don’t want to create and maintain dozens of translators’ accounts for roughly 30 languages. Nor do translators want to work completely manually on texts that follow several brand-related terminology guidelines when there is software that will do the main work for them. Plus, as a translator, you don’t want to copy&paste translated articles as your main job. Therefore: export the master text, import the translations.

And why extra approval process: isn’t Contentful working on an approval process? Yes they are, see link dump at the end of the article for details.

However, again user (and roles) management were an issue, and we had a more elaborate approval processes.

What you need to know beforehand

For the sake of brevity, I will assume that you know Javascript, npm and React better than I did (which was - and still is - embarrassingly little), and that you have a basic understanding of the Contentful APIs (mainly Content Management API and also the Content Delivery API).

From the offered range of Contentful UI Extension types, we focused mostly on the sidebar extensions, field extensions and dialog extensions. We had no need for the page and entry extensions.

JSONs used in the images of this article are simplified for shortness.

Our setup

Initially we attempted to create everything inside Contentful including extra content models for workflow steps, for approval information, etc. And it absolutely did work. But it soon turned out to become a referencing labyrinth designed in Hell and it significantly slowed down overall performance of an entry page.

Instead, we decided on a SpringBoot application with a database as tiny backend to keep our additional meta data for the articles, some user data, which could easily integrate authentication solutions later on.

And in order to make the Contentful users, in our case called Content Editors, as comfortable as possible, we decided to use the UI Extensions from Contentful to ensure that users can fully work within Contentful independently of the steps happening inside or outside Contentful.

For the development of the Contentful UI Extensions, we used the create-contentful-extension CLI which saves you a significant time and effort. Install the CLI, type the command to create a new extension and it will guide you through a step-by-step creation saving you a lot of configuration trouble.

In our setup, all our text content is kept inside Contentful; no content is duplicated in our tiny backend. Contenful is the single source of truth regarding text.



Content model

Our article was mainly an entry with a lot of references. We added a short name field without i18n for use within Contentful. In Contentful the modelling structure of an entry is called content type, below find our examples.

This is how the main article content type looked like:

Article content type

{  "name": "Article",  "description": "Content Model of main article document",  "displayField": "article",  "fields": [    {      "id": "articleName",      "name": "Internal article name",      "type": "Symbol",      "localized": false,      "required": true,      "validations": [        {          "unique": true        }      ],      "disabled": false,      "omitted": false    },    {      "id": "articleHeader",      "name": "Article header meta data",      "type": "Link",      "localized": false,      "required": false,      "validations": [        {          "linkContentType": [            "articleHeader"          ]        }      ],      "disabled": false,      "omitted": false,      "linkType": "Entry"    },    {      "id": "masterBlocks",      "name": "Main section blocks",      "type": "Array",      "localized": false,      "required": false,      "validations": [],      "disabled": false,      "omitted": false,      "items": {        "type": "Link",        "validations": [          {            "linkContentType": [              "masterBlock"            ]          }        ],        "linkType": "Entry"      }    }  ]}

The article header content type only contains a few localized fields for further describing the article and is not shown here.

The master block content type is a container for further references, but allows the user to add an additional headline for further structuring an article.

This is the content type of a master block:

Master Block content type

{  "name": "MasterBlock",  "description": "Main element of an article, should contain further sections",  "displayField": "headline",  "fields": [    {      "id": "headline",      "name": "Headline",      "type": "Symbol",      "localized": true,      "required": false,      "validations": [],      "disabled": false,      "omitted": false    },    {      "id": "sections",      "name": "Sections",      "type": "Array",      "localized": false,      "required": false,      "validations": [],      "disabled": false,      "omitted": false,      "items": {        "type": "Link",        "validations": [          {            "linkContentType": [              "formattedTextBlock",              "textMediaBlock",              "plainTextBlock"            ]          }        ],        "linkType": "Entry"      }    }  ]}

Sections is an array of 1 to n references to content types which contain localizable content (text, formatted text, text with media assets).

Additional meta data in backend

In the database we keep only references to the main article, the version, and the data regarding the process steps. Like timestamp of approval process trigger, data for received approvals, etc.

This is a rough schema of the database:


What did we learn?

Local development

The create-contentful-extension library allows you to install an extension from your local machine, which speeds up initial development, but can become confusing when you have several developers working on different extensions. If the other developers are currently not running their own servers, you end up with some extension error messages, and you have to enable unsafe connections in your browser for the page.

Just set your .contentful.rc file in your extension folder to your Contentful space, make sure you enter the correct environment ID and add the correct API management token and do a

npm run start

in the folder of your extension to get auto reload when you change your code in your IDE. Just make sure to run a

npm run deploy

after finishing if you want to run the extension as Contentful hosted, otherwise you probably already set up your own deployment routine.

With the recent updates of both Firefox and Chrome regarding mixed content security, it is best to set the local development to startup in secure mode. The easiest way to achieve this is to edit the package.json of your extension and append the --https parameter to your start script, or run it as npm run start –https

"scripts": {    "start": "contentful-extension-scripts start  --https",    "build": "contentful-extension-scripts build",    "lint": "eslint ./ --ext .js,.jsx,.ts,.tsx",    ...  },


We initially wanted to use the internal Contentful versioning of an article to ensure that a translation still matches the correct master text of one article.

Unfortunately, a change to any referenced items within the entry does not count as a change to the referencing entry. Meaning a change to the ArticleHeader item does not increase the version of the article itself. Also, in our case, we were happy to ignore some changes in the main article as long as they did not affect the i18n relevant parts of the entry.

Blog post contentful extensions_parent_doc_child

In this example, changes to contents of entry with the id ‘3whM8mqfSBsZjUlJKN8SSn’ do not affect the parent document, which only references the entry.

So we ended up having to maintain an extra version number and implement checks when to increase the extra version independent of the internal Contentful version number.

Unique, but only for publishing

If you configure a content model via Contentful UI, you see in the validation tab the checkboxes to flag an attribute as required and unique. And reading the description, you see that these attributes only affect the publishing of an article. You can always save entries without a title or duplicate titles while the entries are still in draft state. Since we do not use the in-built publishing mechanics, this is unfortunate because we had to implement our own validation.

IFrame limitations

UI Extensions are loaded via sandboxed iframe tag, and not all of the allow flags are enabled. For example, you cannot open pop-up windows from within the extension, if you want to open a modal window, you have to use the modal dialog extensions library, and you cannot access the parent’s session storage.

Hosting considerations

Contentful allows you to host your extensions with 3rd party hosting, which lets you avoid the extension size limit of 512 KB. You can simply dump your extensions into an S3 bucket or any other server of your choice (just make sure you take care of CORS and the https certificates). Third party hosting also allows you to set up your own CI/CD pipeline. For small extensions, that are pretty stable and not prone to change, you can easily use the Contentful hosting for a most convenient way of deploying extensions.

Rate limits

Always, always keep an eye on your Contentful API calls. The API rate limit is not really low, but you have still to watch it. Depending on your contract, you might hit it faster than you imagine. For example the rate limits are counted cross-spaces, so if a team works a lot on a space you do not see, it will still affect the overall API call count. Consider caching as one option or find other ways of reducing your API calls.

One option to reduce calls is how you structure the extensions. We started out with each component in a separate extension, which increased our extension count rather quickly and also made us repeat a certain amount of calls and code. It was easier for us regarding overview, but the duplication started to create its own problems. Developers more familiar with npm might avoid some of these issues by creating private packages as dependency for specific extensions. We preferred to merge our sidebar extensions since we chose 3rd party hosting, and did not have to care about extensions size limit this way.

Another small advantage of merging the extensions into one big sidebar extension: better control over the workflow interface. You cannot completely hide a sidebar extension. It will always show at least the title of the extension in the sidebar. This merger allowed us to completely hide and show buttons / text when the process called for them.

Export / Import

With our article content model providing a lot of structure flexibility to the editor, we had to pay the price for exporting the article for translation. While it is possible to create a JSON representation of an entry, we wanted to create a file with as little superfluous data as possible, and in a structure that could easily be imported. The extension that takes care of creating a flat and simple JSON was pretty rate heavy, because we had to follow each reference, check the content type of the referenced item, retrieve the text contents in the default language, throw them into a new object, and let the user download a JSON file.

Contentful-extensions-blog-post-Smallest of all export 1Contentful-extensions-blog-post-Smallest of all export 2.1

But in this way we went from chaotic nesting in the file to a much flatter and shorter version.

Importing is still rate heavy because for one translation, we had to create all the language versions of the referenced entries.

Dialog extensions are nice, but not intuitive

The dialog extensions are quite powerful, once you get the hang of them.

You can pass in parameters while opening them, and you can return data in their resolved promise. You can navigate to entries or assets, you can have modals, alerts, everything short of a dialog window hopping over your screen.

But… you set a width parameter to open a dialog extension, but not a height parameter. You can set a minHeight parameter instead. So you let your dialog extension update the height in the extension’s initialize method. I am sure there is a reason, I just can’t see it.

The alert dialogs are easy to use for quick feedback. Keep in mind though that they only allow plain text as message body, you cannot apply formatting or line feeds, so make sure to use it only for short messages.

Type of extension is independent of complexity

Just because something is called a field extension does not mean it has to be just a custom field. We turned a field extension into a full table with sorting and editing. So we added a new property to our article content type with a short text called ‘approvals’, and selected the new extension in the appearance tab and had a full width table rendered in the Contentful editing window. On the other hand we had sidebar extensions that just contained a button and triggered a post request to our tiny backend.

Field type vs. field extension

What can be confusing about the field extensions is the assignment to field types. While you can choose from S_ymbol(s), Text, RichText, Integer, Number, Boolean, Date, Location, Object, Entry/Entries, Asset(s)_, you can configure a field to be a Rich Text, Text, Number, Date and Time, Location, Media, Boolean, JSON object and Reference. It does not add up, but a symbol is actually the short text. Thankfully, you can create a content type as cheat sheet to see how different field configurations are mapped to types.

Or you use our table as cheat sheet:

Look & Feel

We wanted to seamlessly integrate our extensions into the Contentful interface in order to make the Content Editor feel comfortable. I soon found myself despairing.

The Forma 36 library provides most components, but lacks details in the documentation, e.g., how to add correct sorting styling to a table head. And while it’s possible to check the open source project on GitHub and view the components in Storybook, it’s time consuming and - depending on your experience level - not always helpful.

In the picture below, you see the difference between the Contentful table in the overview of your documents and our custom table below. I could not figure out a way to achieve the same look and feel for emphasizing the sorting in the table header.


Feedback messages may also be an issue. There is no access to a “global” notification message that is always on top of the entry editor and easy to position. So notifications are shown within their extension, and if your extension is just a short text with a button in the sidebar, the notification message is more or less invisible to the user. We had to work a lot with the bigger dialog windows instead.


Yes, all developers know that documentation is a pain in the, ahhh, prefrontal cortex? That would explain why we so quickly forget to do it …

Anyway, depending on your product, more effort might be necessary.

So the extension SDK documentation could be more elaborate, especially for beginners like me.

To be fair, the standard management, delivery and asset API docs are in a better state, but for using the Java SDK, I found it easiest to check the test classes in the GitHub project to find out how it is supposed to work. Although I do not know about the documentation state of the other platform SDKs. The Ruby version, for example, has more example apps and more tutorials.


Contentful undoubtedly offers you many options. The extensions in combination with clever modelling of your content types, and other Contentful features such as webhooks allow a lot of freedom.

But: just because you can, does not mean you have to!

While we could complete a PoC with every implemented within Contentful, we decided against it. Although it was nice to see that it’s possible, there is a tool for every trade. And Contentful is a great tool to store all things content, but not for storing meta data to the extent we needed for our use case.

Meanwhile, if that got you interested in the UI Extensions, keep an eye out for Contentful Apps. A lot is happening there, which will strengthen extensibility even more.

Do not feel shy about contacting Contentful support. They are really great people and extremely helpful.

Understand the limits of your contract regarding rate limitations, remain patient and don’t be afraid to dig into their GitHub repositories for a better understanding.

Further links

Contentful APIs:

Approval / Collaboration workflows:

Create your own extensions:


One of the developers told us how they are currently working hard on apps, so there is more to come soon.

Also make sure to occasionally check their blog

Photo on Unsplash