Contentful Headless CMS - l10n & i18n


It seems there is a new buzzword in the website building industry: headless CMS (Content Management System). But what is exactly a headless CMS and how does it work?

 In this write-up, I am going to walk you through the different steps of how I explored one of the most popular headless CMSs, Contentful; how I created a simple website with it; and what approaches I took to localize the website into another language.

In the end, I realized that I needed a completely different mindset on website building and localization in general. And I had a clear vision of when a headless CMS is useful and when it is recommended to use a traditional CMS, like WordPress, instead. Finally, I learned a lot about Node.js, Gatbsy.js, website deployment, and the many challenges i18n problems introduced into the subject.

Part 1: Headless CMS & contentful:

What is headless CMS?

A headless CMS is a platform that has no default front-end system to determine how the content is presented to the end-user. Instead, a headless CMS is front-end agnostic, meaning that the content is raw and can be published anywhere, through any framework.

By getting rid of the front-end delivery layer, the CMS is suddenly a content-only data source. It produces content and then sits there. Waiting.

Waiting because there is no default “head”. Front-end developers are free to build as many heads as they like, for however many channels they want to serve content to (think websites, apps, kiosks, billboards, smartwatches, etc). To retrieve the content for each channel, the headless CMS responds to API calls.


Contentful is a headless CMS that allows content to be organized into what could be called “modules,” or little bits of data that can be rearranged to appear nicely on mobile devices, tablets, computers, virtual reality devices (maybe someday?) and more.

The way Contentful handles bits of content means that the content can be pushed out when new technology develops without having to redesign, rewrite, or rethink all of it for a new format.

Content vs. Context

Let's talk a bit about content and context. Content is everywhere. From posts to podcasts, we are creating and consuming content all the time in our personal and professional lives.

From the point of view of content creators, it is often necessary to focus too much on where the content will live. They fixate on an interface: a website, a mobile device, a digital screen. This interface-centric thinking often leads to endless redesigns, siloed and compromised content, and essentially a waste of money and time.

To understand the problem more, we need to separate content from context. Content is the pieces of data, such as the title, subtitle, image, etc. The context on the other hand is the mobile device, a computer, a newspaper, or any other interface. And here is where a headless CMS comes into the picture.

Content Modeling

There are many benefits to separating content from the interface, what we call structured content:


Speed and Cost


"Structured content is a content that is planned, developed, and connected outside an interface so that it's ready for any interface. It allows you to define the skeleton of your content before you create it. Breaking content into the smallest pieces possible (within reason) so that it is free to go anywhere, anytime." Mike Atherton and Carrie Hane, Designing Connected Content

In Contentful structured models are called Content Models.

Contentful vocabulary:

Content Model: the overall architecture of the content.

Content Type: the structure or a container for a piece of content.

Fields: different properties for a piece of content.

Entry: a piece of content based on a content type.

Reference: a link between two content types.

Unstructured Content Model

Well-structured Content Model

Part 2: the localization scenario:

After studying all the sophisticated features Contentful offers, I was eager to put the tool into practice and test out the features, including exploring localization and translation solutions within the software.

My team and I chose the Associação Saúde Criança Porto Alegre (ASCPOA)'s Portuguese website and decided to translate and localize the site into English. We quickly prepared the bilingual text and localized the site images using Adobe PhotoShop.

So, remember when I mentioned in the introduction that Contentful requires a different mindset than how you would normally approach a traditional CMS? That seemed even more true when I started to build the site.

Building a simple website with Contentful+Gatsby.js

Because Contentful is the back-end, we needed to find a front-end for our project in order to be able to see and show what we created. This is definitely one of the features of Contentful or any headless CMSs which needs to be considered when starting a project. Although Contentful offers an intelligent editing tool, you still won't be able to see the "real" changes until you connect it to a front-end.

What is Gatsby.js?

Gatsby is an open-source static website generator (SSG) that is based on the fron-tend development framework React and makes use of Webpack and GraphQL technology. It can be used to build static sites which are progressive web apps, follow the latest web standards, and are optimized for speed and security. In order to use Gatsby, I also needed to download Node.js, and open-source server environment that easily builds fast and scalable network applications.

Creating the site

We used Gatsby front-end to create a simple static website using their Contentful template.

Gatsby is a static site generator that uses the power of React to build any type of website. There is a big difference between dynamic and static websites. A static website contains web pages with fixed content, that is, only Javascript, HTML, and CSS. There is a server that hosts those static assets, but it does not have an engine to run scripts (node, python, etc.). However, there might be content retrieved from other sources other than the webserver hosting the site.

In terms of localization, a way to show a page in a localized view with the minimum duplication possible is needed. For static sites, there may be some duplication as the content is rendered to HTML on deployment. Our job was to make it easy to extend and reuse.


Create a new site in Gatsby and name it. This will create a folder for your GitHub repository.

Connect Contentful as your CMS provider.

Start your site on Gatsby cloud!

Gatsby created a space in your chosen Contentful account with the default content types and content. We are going to use this as a skeleton for our project to create a simple static website that has the main page (Home) and another page for different posts (Blog) that we will use for other pages.

The next step is to head over to GitHub and clone the site into our computer server.

Let's create a new project in the terminal:

You have several choices: copy the code in your computer terminal; download the folder to your computer; or if you have a GitHub desktop, open it there directly.

I am going to copy the code:

type in:

gatsby new [name of your folder or site] [copy link from Github]

If this command is not recognized, you need to install gatsby CLI first. We do a global install by running:

npm install -g gatsby-cli

After Gatsby CLI (or Command Line Interface) was installed, we can run the command again. This will install our site package to the computer.

The Gastby-Contentful site was successfully bootstrapped. Before we run the develop and build commands, we need to insert the API tokens from Contentful.

Enter yarn run setup.

This will help us to configure our space ID and other Contentful API access tokens to connect our code with the right Contentful space.

To add the tokens, we need to go to Contentful-Settings-API keys and copy-paste the token IDs following the prompts.

Some sources suggest including the API tokens directly in the gatsby-config.js directory. We can do that although I found that in some cases this is not the best idea. Your GitHub repository is open source and by adding the APIs directly to the code, you will expose your site's configuration to virtually everyone. Now, Contentful says that the APIs are read-only, still, if you want to avoid making them directly accessible, there are some alternatives. One is to use an environment in the code and in Contentful. This was already created in Contentful by default called "main".

Hint: In Gatsby, you will want to complete the setup and add the missing tokens to have everything neatly organized and in case you choose to deploy the site (I used Netlify).

We created two content types, four entries, and one locale for now.

Once I added the Contentful space ID, the access management token, and the delivery token, we can run gatsby develop (or npm run develop) to start a development server on our local machine.

Now our new site is running on localhost:8000

This is still our basic site template but we can go ahead now and start including content in Contentful as well as modifying the site via Visual Studio Code.

Adding Content to the Site

Once the site is up and running, it's time to start adding content to it. Much can be said about creating content in Contentful, instead, I just add a few pictures and slides to demonstrate the evolution of a site-recreation process.

Translation and Localization

Although the Contentful interface is fairly intuitive and they made it easy for content creators to add another language and do translation, we need to do some setups in order to create a route for our different locale. Otherwise, if we enable localization without an initial setup, Contentful will start putting out content without knowing what to do with it and will double all our articles on one page which will eventually break our code.

For our project, we wanted to localize a simple website from English into Portuguese. We had previously prepared the translation and localized the site images, so we were ready to add the localized content into Contentful.

Localization and translation in Contentful are very easy.

Under settings, we add the locale to the space. We enable the locale in the specified Content model. This is very useful because we got to choose which entries we want to translate. For example, titles to be translated for every blog post but not the author.

Once the locale has been set up and enabled, adding translations is done with the user-friendly and sophisticated entry editor side by side with the source content. We can add permissions to different fields to different translators and editors. There is also a comment field for notes and messages and an autosave option.

We can give translators limited access to the content they need, and only the content they need. Contentful also has a comments where content creators, translators, and editors can leave messages to each other which allows QA to be done on the platform. And there is an autosave, so we don't need to worry about losing our work.

Contentful also integrates directly with various TMSs and translations services providers such as Smartling, Lokalise, Phrase, MemSource, XTM, Lilt, Lokize, GlobalLink, Acclaro, etc. Through their API tokens, integrations are possible virtually with every system and the translations do not need to happen directly in Contentful.

However, because we wanted to demonstrate the whole lifecycle of a localization workflow and we used Gatsby.js as a frontend, we needed to get down to a code level and find a solution to internationalize our Gatsby template so that our content from Contentful could be displayed seamlessly.

A. Using the gatsby-plugin-i18n

For our pages, we added the gatsby-plugin-i18n

yarn add gatsby-plugin-i18n or npm add gatsby-plugin-i18n

This plugin does not translate or format your content, but rather it creates routes for each language, allowing Google to more easily find the correct version of your site, and if you need to, to designate alternative UI layouts.

Let's configure our plugin:

In Visual Studio Code we open our gatsby-config.js folder and add this configuration to the end:

// Add to gatsby-config.js

plugins: [


    resolve: 'gatsby-plugin-i18n',

    options: {        

      langKeyDefault: 'en',

      useLangKeyLayout: false




This configuration sets up the default language (which is English), and it makes the same layout for all pages.

Hint: this may need to be set up differently if you would like to localize your site to a language that reads from right to left for example.

This will create separate files for each language we are translating into.

Do you also remember when I told you that slugs will become important when we need to localize our sites? Here we are telling the program to identify the different language pages based on their slugs.

Now we run gatsby develop again to see if it still works.

Hint: Setting up useLangKeylayout:

true: use a different layout for each langKey (src/layouts/en.js, src/layouts/pt.js, ...)

false: use default layout (src/layouts/index.js)

We set up our key to false, so we don't need to rename to English pages but if the key was 'true', we would need to rename the pages as follows:

index.js to index.en.js

blog.js to blog.en.js

blog-post.js to blog-post.en.js

If it does not work for you, and you receive this error, that means your pages are not named correctly after the configuration.

Also, sometimes we need to clean the cache with gatsby clean.

Next, define our languages in /src/data/languages.js:

module.exports = {

  langs: ['en_us', 'pt_br'],

  defaultLangKey: 'en_us',


Note that this is where the path prefixes are defined (e.g. Also remember that in gatsby-config.js, we set prefixDefault to false, which means that our default English site will appear without a language prefix (e.g.

Now we are ready to address our pages on a case-by-case basis. In our example, we rename /src/pages/index.js (homepage) to /src/pages/blog.js and /src/templates/blog-post.js to /src/pages/index.en_us.js. etc. Then copy these files into /src/pages/index.pt_br.js and /src/pages/about.pt_br.js, respectively, and replace the English copy with Portuguese.

Creating a language switcher

Now we want to render a language picker in the navigation. This should include a simple list of links to the localized version of the current page.

To get this list of links, in your /src/components/layout.js file:

// src/components/layout.js

import React, { Component } from 'react'

import { getCurrentLangKey, getLangs, getUrlForLang } from 'ptz-i18n'


class Template extends Component {

  constructor(props) {


    const { location, siteLanguages } = this.props

    const url = location.pathname

    // const siteLanguages = require('./src/data/languages')

    const { langs, defaultLangKey } = siteLanguages

    this.langKey = getCurrentLangKey(langs, defaultLangKey, url)

    this.homeLink = this.langKey === 'en-us' ? '/' : `/${this.langKey}/`

    this.langsMenu = getLangs(



      getUrlForLang(this.homeLink, url)


    this.langsMenu.forEach(lang => {

      if (lang.langKey === 'en-us') { ='/en-us/', '/')





Although I think this solution is the best for enabling continuous localization on the site, we concluded that having a dev team helping you with a Contentful project is essential. Whether it was something about outdated plugins or just not being able to fix a simple code break (let me know if it worked for you!), we were not able to pull out the results with this option.

So we decided to explore some other solutions.

B. Gatsby-Contentful starter i18n template

The second option was to try to use an already established i18n Gatsby+Contentful template called gatsby-starter-contentful-i18n. The sample site would look like this:

This seemed like a perfect alternative way except that this starter was also outdated and we could not even start to run it on Gatsby cloud. We reached out to support and they confirmed that even if we manage to update the version in the code, most likely the code will still break.

C. Using a multilingual Gatsby template

After all this, we found another template that was already localized to two languages. It seemed pretty neat and in the end, this template finally ran on our server.

We made a few basic changes in CSS for the style and replaced the in the HTLM and the site was running!

However, we still needed to connect the site to Contentful so that it will eventually receive the content directly from Contentful instead of it needing to be included in .md files in the code. Our content modeling skills came in handy at this point because we needed to recreate the content model in Contentful based on the site we had. And finally, to include the JSON configuration into the code by creating a folder called "Contentful" and an export.json file in it.

Part 4: Conclusion:

Headless or not headless?

I must say that at end of this project, I really fell in love with Contentful and the whole world that is connected to it from creating content to developing simple sites; so much so that I went ahead and created my own e-portfolio. If you are reading this article, there is a good chance that you just visited my Contentful+Gatbsy+Netlify backed-up website which is very simple but super-fast and clean.

As for the localization and i18n part, I completely understand now why switching to headless makes sense for a big enterprise that has a global presence and presents its content through various displays and platforms, and they have the budget to keep a dedicated team of content creators and web developers as well as a localization department. However, it also became clear that for localizing a simple site for a company that is just starting out to enter into the multilingual market and only plans to have a website, which is maybe mobile friendly but nothing more, a traditional CMS would work much better unless they want to have a very fast static website.

The people behind the scenes in Contentful and Gatsby promised that they will come up with new answers and solutions, including probably creating a new i18n template or plugins. And I have been ultimately convinced that "headless is the future".

Author: Annamaria Szvoboda, May 10, 2021