The most frequently asked questions in DevRel when I started with Contentful were about how to display links or linked entries and assets inside the Contentful Rich Text field on the front end. It’s no secret that those of you who tuned in to my Twitch streams after I had started at Contentful saw me struggle with the concept of links as well! So, I set out to explore and investigate the inner workings of the Contentful REST API and GraphQL API in terms of linking assets and entries on a content type in order to understand how we can render links inside Contentful Rich Text fields.
What are links in Contentful?
If you’re looking for information on how to render linked assets and entries returned as part of the Contentful Rich Text field response using REST or GraphQL in JavaScript, check out this post.
Links are Contentful’s way of modelling relationships between content types and entries. Entries in Contentful can contain link fields that point to other assets or entries, and those entries can link to other assets or entries, and so on. For example:
- A blog post can have an author
- A team can have many authors
- A company can have many teams
You can liken this to working with relational databases, where you would define one to one or one to many relationships within your data structures or models. For more information on the concept of links in Contentful, visit the documentation.
Here’s the content model that we’ll be working with in this article. The screenshot of the blog post content model below shows that the Author field is a Reference field type, which is a link.
TL;DR:
If you’re using the Content Delivery and Content Preview REST API, Contentful provides a number of SDKs (Software Development Kits) in the most popular programming languages. These will resolve your linked entries and assets for you. In this example, we’ll be taking a look at the JavaScript SDK.
If you’re using the GraphQL API, you control how your entries are resolved in the construction of your GraphQL query. And by understanding how the REST API works and how the SDKs resolve links, you’ll be all set.
Let’s take a look!
Requesting data from Contentful
The following examples focus on using the JavaScript ecosystem to query data from this example blog post. The example blog post is served on an application built with Next.js — but we won’t be going into Next.js in this post.
Using the REST API
Take this example request URL.
https://cdn.contentful.com/spaces/{{spaceId}}/environments/master/entries?access_token={{accessToken}}&content_type=blogPost&fields.slug=the-power-of-the-contentful-rich-text-field&include=10
It is querying the Contentful Delivery API with the following parameters:
- spaceId: Our space ID
- accessToken: Our access token for the Content Delivery API
- content_type: blogPost
- fields.slug: the-power-of-the-contentful-rich-text-field (return the blogPost entry that has this slug)
-
include: 10 (return linked entries and assets up to 10 levels deep (this is the maximum
include
parameter value on the Content Delivery API) - we will unpack this later!)
The REST API response
The raw JSON response from the request above contains the following top level properties and nodes in a flat structure.
{
"sys": {
"type": "Array"
},
"total": 1,
"skip": 0,
"limit": 100,
"items": [...],
"includes: {...}
}
The items array
items
contains the requested entries (the entry with the matching slug in this case). Each entry contains a subset of the fields
defined on the content type of this entry and some internal system information (sys
). Notice how the linked author
entry is missing the fields property. It only holds the sys
information including the linkType
and id
.
"items": [
{
"sys": {...},
"fields": {
"title": "...",
"slug": "the-power-of-the-contentful-rich-text-field",
"author": {
# This is a "Link"
# and contains only a reference to the Author entry
"sys": {
"type": "Link",
"linkType": "Entry",
"id": "123456789"
}
},
}
}
]
Where are the author fields? Let’s find out!
The includes object
The includes
object contains two array nodes:
-
"Entry"
for all referenced entries initems
(such as the the blog post author which we saw returned as a“type”: “Link”
in the response above) -
"Asset"
for all referenced assets initems
(such as images, which might be a featured image on a blog post, for example)
In the case of the author
, which is a linked entry on our blogPost
, we see the full author object returned in includes.Entry[0]
— including another link to an image asset.
"includes": {
"Entry": [
{
"sys": {
"space": {
"sys": { //... }
},
"id": "123456789",
"type": "Entry",
"createdAt": "...",
"updatedAt": "...",
"environment": {
"sys": { //... }
},
"revision": 1,
"contentType": {
"sys": {
"type": "Link",
"linkType": "ContentType",
"id": "person"
}
},
"locale": "en-US"
},
"fields": {
"image": {
"sys": {
# Here’s another link that we didn’t find in the items array
# due to it being nested deeper than 1 level in the object tree
"type": "Link",
"linkType": "Asset",
"id": "555555555"
}
},
"name": "Salma Alam-Naylor",
"description": "This is the author description",
"twitterUsername": "whitep4nth3r",
"gitHubUsername": "whitep4nth3r",
"twitchUsername": "whitep4nth3r",
"websiteUrl": "https://whitep4nth3r.com"
}
},
]
}
The response includes all the data that you need to render the blog post to the front end. However, the data is spread across items
and includes
, and you — as a developer — would expect all that data to be returned as one object, right? 🤯
For example, in React, you might want to do something like this to show the author’s name on the front end:
export default function BlogPost(props) {
const { blogPost } = props;
return (
<div>
<h1>{blogPost.fields.title}</h1>
<h2>By {blogPost.fields.author.name}</h2>
</div>
);
}
However, we need to do some more work before we can make this happen — we need to resolve the linked entries — and this is where we can use the Contentful JavaScript SDK.
Currently, the blogPost item references the author by sys.id
:
"author": {
"sys": {
"type": "Link",
"linkType": "Entry",
"id": "123456789"
}
}
You could cross-reference the items[0].fields.author.sys.id
with the includes.Entry
array, find the item in the array that has the id
that matches, and resolve the data from there. It sounds pretty straightforward in this example, but when your content model gets more complex with many entries linking to other entries, it could get unwieldy.
Let’s look at how the JavaScript SDK can help us out.
Under the hood, the JavaScript SDK uses the contentful-resolve-response package, which converts the raw nodes into a rich tree of data. The one limitation of the Contentful Delivery API to bear in mind is that it will only return linked entries up to a maximum of 10 levels deep that can be resolved.
Unpacking the include
request parameter
Specify the depth of the resolved tree using the include
parameter in the request to the API, either as a parameter on the GET request URL, like this:
https://cdn.contentful.com/spaces/{{spaceId}}/environments/master/entries?access_token={{accessToken}}&content_type=blogPost&fields.slug=the-power-of-the-contentful-rich-text-field&limit=1&include=10
or via a call to the JavaScript SDK:
const post = await client
.getEntries({
content_type: "blogPost",
limit: 1,
include: 10,
"fields.slug": "the-power-of-the-contentful-rich-text-field",
})
.then((entry) => entry)
.catch(console.error);
Both examples above make the same request to the Contentful API — except the SDK example is resolving your linked entries as part of the process using contentful-resolve-response. Neat!
How the include
parameter affects the length of the includes
response
Say you have a blog post, which has a reference to an author, which has a reference to a team.
To visualise this in an object graph:
{
"blogPost": {
//...
"fields": {
"author": {
//...
"team": {
//...
}
}
}
}
}
If you specify includes=1
in your request, your includes
array on the response will contain one item in this example, the author
object (1 level deep).
If you specify includes=2
in your request, your includes
array on the response will contain two items, the author
object and the team
object. (2 levels deep).
If your blogPost
had another top level reference, say a heroBanner
, includes=1
would return both the author
and heroBanner
inside the includes
array.
{
"blogPost": {
//...
"fields": {
//...
"heroBanner": {
//...
},
"author": {
//...
"team": {
//...
}
}
}
}
}
Regardless of the include
depth you specify — the SDK — which uses the contentful-resolve-response package, will link all available and responded entries and assets that are returned in the includes
response.
Read more about the include param on the Contentful docs.
Using the GraphQL API
The Contentful GraphQL API doesn’t require an SDK to handle linked entries — but understanding the concepts covered previously helps us out here.
The main differences between the REST API and GraphQL API
- The response from the GraphQL API gives you a rich object graph as standard (so you won’t find
includes
in the response). - With GraphQL you specify the equivalent depth of the
includes
response through the construction of your query. The only limit here is the complexity of your GraphQL query. Technically, if you construct your query cleverly, you can reach data hundreds of levels deep! Read more about GraphQL complexity limits here.
Here’s the GraphQL query that we would use to fetch the same blog post data with the author name and image as referenced in the first example:
const query = `{
blogPostCollection(limit: 1, where: {slug: "the-power-of-the-contentful-rich-text-field"}) {
items {
sys {
id
}
title
slug
author {
name
# more author fields …
image {
sys {
id
}
url
# more image fields ...
}
}
}
}
}`;
And here’s how we can query the Contentful GraphQL API using fetch:
const fetchOptions = {
method: "POST",
headers: {
Authorization: `Bearer ${ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query }),
};
const response = await fetch(`https://graphql.contentful.com/content/v1/spaces/{SPACE_ID}`, fetchOptions).then((response) => response.json());
To compare this query to the include
levels in the REST API:
- Level 1:
blogPost
- Level 2:
blogPost.author
- Level 3:
blogPost.author.image
The GraphQL API response
Due to how we constructed our GraphQL query to fetch the linked entries and assets, the raw response from the GraphQL contains the data for the linked assets and entries in the nodes we would expect — at a content type level only.
Here’s the response for the above query from the GraphQL API:
{
"data": {
"blogPostCollection": {
"items": [
{
"sys": {
"id": "53PLFh5VLIotcvMqR6VsnO"
},
"title": "The power of the Contentful Rich Text field",
"slug": "the-power-of-the-contentful-rich-text-field",
"author": {
"name": "Salma Alam-Naylor",
"image": {
"sys": {
"id": "rImaN1nOhnl7aJ4OYwbOp"
},
"url": "https://images.ctfassets.net/.../image.png",
}
},
}
]
}
}
}
In the response above, the data for author
appeared in the node tree exactly where we expected it, and we can access the name on the front end — for example, via data.blogPostCollection.items[0].author.name
— without having to use an SDK to resolve the entries.
The include depth is inferred by the construction of your GraphQL query
In comparison to the REST API, where you usually fetch the blog post data and link the entries after the fact, a GraphQL API query is entirely flexible to your needs. There’s always the caveat, however, that a complex GraphQL query with many nested link assets and entries might surpass the maximum complexity permitted on the GraphQL API. Read more about GraphQL complexity limits here.
In conclusion
Understanding the structure of the data responses from Contentful and how linked assets are returned and then resolved via the Contentful SDKs, empowers you to choose which APIs and methods are best suited to your applications. And, hey, if you want to resolve the linked assets and entries yourself, then you’re well equipped.
Check out some further reading on how you can resolve linked assets and entries from the Contentful Rich Text field response in both the REST API and GraphQL API.
And remember, build stuff, learn things and love what you do.
Top comments (2)
Very helpful article, I followed you on here and twitter, Looking forward to reading more!
Welcome to my weird places on the internet, Monty!