Implementing Contentful
⚠️ This recipe is out-of-date. It needs to be updated in regards to
i18n
and to prioritize the usage ofgetStaticProps
/getServerSideProps
methods. If you are going to use use Contentful in your next project, please upgrade this recipe.
This recipe aims to guide you through the implementation of Contentful in the project and also provides custom solutions for different use-cases.
What is a CMS?
A CMS, or Content Management System, is a platform that helps in the creation and management of content to be consumed in a web application. In short, these services provide a database and a database management system with a layer of usability on top that makes them inclusive to users without technical expertise.
Contentful
Contentful is one such CMS, and the object of this recipe.
For more information about Contentful's API please refer to their documentation.
Modeling the schema
As a developer working on the App that will consume the content stored in the CMS, you're also responsible for creating and modeling the schema itself. This will mean creating new content models that must be usable by the client team which will eventually handle Contentful after the hand off of the project. One major consideration to have is the usability and readability of the content models you're creating. Remember that they are meant to be usable without any previous technical knowledge of this context.
Contentful offers many options for type validations, input appearance and descriptions to help the users understand what kind of input is expected. You should use all of them to minimize the chance of users not understanding what's expected of them, in what ways they are limited, reducing the chances of unexpected content in your application.
Keep in mind:
→ Always use appropriate types when possible,
→ Use validations where appropriate to make sure the input values are what the developer expects,
→ Use meaningful titles and descriptions to ensure the user knows what's expected of that field.
Localization
Contentful makes it easy to add localized content to your application. Once you define a locale, which you can do in the Locales option in the Settings menu, you can specify in each field you create whether they can be localized, in what languages, etc., giving you fine control over what needs to be localized and what doesn't.
Once having these locales, you can then use the Contentful API signaling your desired locale through the locale
field. You'll find an example of this in the next section.
Preview
Contentful splits content changes in 3 states:
Published
: when a user saves some content.Changed
: when the user changes previously saved content but still hasn't saved the new one.Draft
: when the user creates new content but doesn't save.
Usually, only Published
content is available, unless a specific request is done. This request is called Preview
and may help users see beforehand how content would look like on their website before publishing it and making it available for every visitor.
However, Contentful does not validate content that is not published. This means that, if your app relies on validation from Contentful, it will likely break in preview mode unless all content in Contentful already passes validation. This is assumed and must taken into account when developing your applications and communicated to potential users.
Walk-through
1. Installing Contentful SDK
Using Contentful in your application can be done using the Contentful SDK available as an npm package, which you can install using the following command:
This SDK provides access to Contentful's Delivery API and Preview API, both of which will be used in the integration of Contentful in your app.
First, you'll need two Contentful access tokens and a space identifier. All three can be found in the Settings menu, under the API keys
option:
- Space ID,
- Delivery token,
- Preview token;
Each token must be used together with their specific host, as follows:
- Delivery host:
cdn.contentful.com
, - Preview host:
preview.contentful.com
;
After obtaining the Contentful access tokens, you need to set these environment variables:
Setting up the SDK can be done as follows:
This client
exposes the functions necessary to fetch the data in Contentful, for example:
If you're taking advantage of localization in your Contentful space, you must add to the request a field with your intended language code, like so:
In the contents you are retrieving, you may have links to other contents. In this case, you will have to decide if you want the SDK to resolve those links for you. This way, instead of a content link being returned in the includes
property of the response, the actual content will be placed in the field where the linked entry metadata would be.
You can change the depth to which links will be resolved with the option include
. By default it is set at 1
, and its maximum value is 10
. Please consider that a higher include
value will introduce more complexity to the resolver and may result in a loss of performance.
2. Populating the app with fetched content
To populate the app with the fetched content, we will explore two possible implementations: one for bigger applications that justify the inclusion of a Redux store, and another which doesn't.
getInitialProps
2.1. Accessing the client directly though In Next.js apps, the getInitialProps
function in general components typically receives a context object, which exposes many aspects of the context in which the page is rendered. Because we are building a custom app, and doing the work of running these getInitialProps
functions ourselves, we can control what data they receive. In practice, we will be populating the context object with the information we want to propagate to our components. So, in your App.js
file, you can check whether the page was loaded with a cms-preview
query parameter, build your client, and share it across the app.
This function also has the advantage of letting you return props to your App function, making it easier to signal the App itself if you're in preview mode.
Afterwards, you can use the Contentful client in your component's getInitialProps
to fetch content required for that component.
2.2. Using Redux
When using Next.js, you can use next-redux-wrapper to integrate Redux in your application. We will be using this wrapper as part of this walkthrough, but will not cover its integration. For more detailed information on this package, e.g. how to install it, please refer to its documentation.
As part of using this package, you'll have to create a function (which we'll call buildStore
) which should return the instance of the Redux store in your application, and it will be here that you will create your client
instance, and decide whether it should fetch published or preview content.
Given the nature of Next.js and the implementation of next-redux-wrapper, buildStore
will run twice, once server-side and once client-side, requiring you to consistently instantiate the client
to be the same for both cases. But the same strategy cannot be used for both. Each requires its own solution.
In the following example we will be checking the presence of a cms-preview
query parameter to decide whether to show preview content or the standard, published content, i.e. something like www.example.com/?cms-preview
.
Then, in your actions scripts, you can use the provided client to fetch the content that you need, like so:
2.3 Rendering Images
When rendering images from Contentful, you should use @moxy/react-contentful-image in order to easily take advantage of Contentful's Image API.
NOTES:
ℹ️ The Contentful Image API provides a CDN when serving the assets so processed images are cached to reduce response time.
ℹ️ The Contentful Image API does not provide compression for raster or vector images at the time of writing (see this and this). As such, we need to make sure the assets uploaded to Contentful are already compressed as much as possible, for example by using tinypng before uploading in the Contentful UI.
Raster images
In order reduce image file size as much as we can, make use of the parameters format
and resize
. The default format is webp which provides low file sizes without major quality losses.
When rendering small logos opt for 8bit
png and resize to the maximum used height:
NOTES:
ℹ️ The component does not at the moment generate srcsets for different screen sizes, see the issue here. Please contribute to @moxy/react-contentful-image in order to have this functionality available to everyone.
Vector (SVGs)
No compression or optimization options can be applied here. So in order to handle SVGs:
- Make sure all SVG files uploaded to contentful are properly compressed and optimized beforehand through a visual editor and SVGOmg.
- When rendering, skip any kind of optimization and use the file from the API as is. You can conditionally render the image or use the helper parameter
optimize
:<ContentfulImageimage={ image }optimize={ !image.url.includes('.svg') }format="8bit png"resize={ { height: 50 } }/>
3. Conditionally rendering DOM elements in case of preview
Since we're on the topic, you must use a similar strategy in your App.js
file to conditionally render a DOM element to let the user know they are in a preview state. This must happen once per instance of the App, since we will not perpetuate the cms-preview
query parameter on route changes.
As an example, we show how to conditionally render ContentfulPreview
, which is a component that consists in a ribbon that is placed on the page to indicate the user is viewing a preview version of the content.
Using hooks:
Class component:
4. CMS Translations
This boilerplate already includes Internationalization using @moxy/next-intl
, this is done by statically configuring individual intl/messages/<locale>.json
per locale. You can do this dynamically by moving this static files to a Contentful content model and by enabling localization in it.
1. Defining the content type
Firstly, define the content type that will consist on a list of key/value pairs. Add the fields necessary to fit your needs, for example homePageTitle
.
ℹ️ Make sure that you enable localization on every field you set in your content type.
After creating your content type populate the respective fields accordingly.
@moxy/next-intl
2. Overriding Now that you have your content type set you should update the /intl/index.js
file to fetch the data from Contentful. This is done by using the Contentful SDK.
Cache
1. Contentful API rate limits
API Rate limits specify the maximum number of requests a client can make to Contentful APIs in a specific time frame. Every request counts against a per second rate limit.
Currently, Contentful doesn't enforce any limits on requests that hit their CDN cache. For requests that do hit the Content Delivery API enforces rate limits of 78 requests per second.
When a client gets rate limited, the API responds with the 429 Too Many Requests
status code and sets the X-Contentful-RateLimit-Reset
header that tells the client when it can make its next request.
2. Custom Caching Layer
One preventive measure to avoid hitting the rate limit for Contentful is to implement your own custom caching layer.
This can be done by setting up a proxy server which will add an s-maxage
HTTP header into the Contentful's response.
This header will then be interpreted by the CDN that is delivering the application (for example CloudFlare), which will cache the response and avoid repeating the same request to Contentful during a specified time interval.
The first thing you'll need in order to implement this solution is to install http-proxy:
Then, you'll want to create an endpoint in your application which will serve as a proxy for all the requests directed at Contentful's API.
For this, you'll have to create the file pages/api/cms/[...cms].js
. This file/directory structure and naming is important because you'll want to receive any requests directed to <hostname>/api/cms/*
.
This endpoint will create the proxy
server and, on each request, remove /api/cms
from the request, rewrite the host
header to the correct host (cdn.contentful.com
), redirect the request to cdn.contentful.com
and set the Cache-Control
HTTP header of the response to s-maxage=60
.
Finally, you will need to setup the Contentful client to direct it's requests to the endpoint you just configured. Please note that you only want to cache the requests when in a production environment.
NOTES:
ℹ️ You have to be able to access to
process.env.NODE_ENV
from the client side as well as the server side, to proxy client side requests to Contentful in production environments.
ℹ️ The
process.env.SITE_URL
variable has to be correctly configured and accessible from both server and client side, otherwise the request to the proxy endpoint will not happen correctly. This may mean that preview url's in merge request will not function correctly, which will happen if the application is started withprocess.env.NODE_ENV
set toproduction
.
Custom SEO
There may be cases where you will want to configure custom SEO per page. Unfortunately, Contentful does not provide out-of-the-box SEO support, so you will need to implement your own strategy. Bellow you can find our approach to the problem.
Create a content model for SEO
To make it easier to configure SEO you need to create a specific content model just for it.
While creating a content model for SEO please do the following:
- Add an entry title field to identify the model, e.g. "Homepage SEO".
- Add a text field called
Title
to be used as the title tag and for the following meta tags: [og:title
,twitter:title
]. - Add a text field called
Description
to be used for the following meta tags: [description
,og:description
,twitter:description
]. - Add a media field called
Image
to be used for the following meta tags: [og:image
,twitter:image
]. - Add a JSON field called
Additional SEO
to provide the possibility to add more meta tags. - Add a many files media field called
Additional SEO Assets
to give the possibility to use CMS images in meta tags.
The resulting content model should look like this:
The Additional SEO
must follow the same structure documented in @moxy/next-seo package but with a small difference. Since Contentful doesn't provide an easy way to obtain an image URL directly from their dashboard, we need to reference the assets we want with the Additional SEO Assets
field and use their ids to specify which one should be used:
By defining content
as an object with an id
property we make the assumption that the content derives from a CMS asset.
In order to obtain the id
you need to select the asset entry and, on the right side panel of the asset, select Info and copy the id. Later in the application you'll use this id to generate the URL for the asset.
Add a link for SEO to other content models
Now that the SEO content model is created, its time to add a reference field type in other models that might need SEO.
Remember to add a validation to only accept an SEO content type.
Preparing the application
On the application itself, the steps to customize the SEO are the following:
- Install @moxy/next-seo.
- Define a default SEO data.
- Render the default SEO data in the
App.js
file using @moxy/next-seo (outside theHead
tag).
Create a component to deal with a Contentful SEO entry
After fetching the page data from Contentful as you normally would, you might now have an SEO field. To be able to render its content in the right manner we suggest you use a ContentfulSeo
component that prepares everything to be rendered with @moxy/next-seo.
The data
it receives is the SEO field in its original state.
So, to render Contentful SEO you just need to do the following:
Where contentfulData
is the data that you have fetched from Contentful.