Integrating Contentful with a Server-Rendered Next.js App? Watch Out for Cycles!

On my software development team, we recently integrated a popular Headless CMS with a popular web framework. It was going well until a latent issue popped up. It took a bit of investigation to understand what was going on, and a little bit of reengineering to fix it. I hope I can save you some time.


Contentful is a prominent Headless CMS. The data you have in your CMS is a graph. It’s totally reasonable and normal to have cycles in it. Page A can link to page B, which can link back to Page A.

Contentful provides access to your content through a few mechanisms: a CLI, a GraphQL API, and a REST API. The REST (“Content Delivery”) API serves JSON, which famously cannot handle cycles.

Naturally, to solve this problem, Contentful’s API splits the data out into separate records, referencing each other by ID. To make it easier for developers, Contentful’s JS library shields you from this. After receiving data from the API, the library will stitch it back together into an object graph, a data structure that your application code can more naturally traverse.

The right side is much nicer to deal with when you’re trying to render content. (Image from Contentful)

So, you get the best of both worlds: valid serialization over the wire, and an ergonomic data structure reconstituted on the receiving end.


One of Next.js’ distinguishing features is its flexible and sophisticated support for multiple types of data fetching, in support of static generation and server-side rendering. Depending on your needs, you can retrieve data for your apps’ pages at compile-time, at runtime on your server, and/or at runtime on your client.

It’s largely automatic and magical. Next blurs the lines between client and server, but you usually don’t have to care. One reason you do have to care is that some of Next’s boundaries rely on JSON serialization, and as mentioned above, JSON can’t serialize cyclical data.

It’s a trap!

Though Contentful can represent cyclical data, whether your data actually contains cycles depends entirely on the content you happen to have authored. In a brand new app, you may have started with a bunch of placeholder Lorem Ipsum data, and you might not have thought about wiring up representative relationships between your fake records. Maybe a few weeks in you’ll introduce a hyperlink cycle and learn that an area of your app is more fragile than you thought.

In particular, my team’s current app accesses the Contentful API from within getServerSideProps() to chew on the results a bit before deciding what to provide to the frontend. Here’s the first flavor of error we encountered:

Error: Error serializing `.circularObject.self` returned from `getServerSideProps` in "/example".
Reason: Circular references cannot be expressed in JSON (references: `.circularObject`).

Here are a couple more varieties you might provoke:

Error: Error serializing `.circularObject.self` returned from `getStaticProps` in "/version".
Reason: Circular references cannot be expressed in JSON (references: `.circularObject`).
Error: Circular structure in "getInitialProps" result of page "/content/[slug]".

Getting Out of the Trap

There are a few ways to get around the problem of Next’s serialization of Contentful’s cyclical content.

Keep it on the client.

If your client browser talks directly to Contentful, then the only JSON interface involved is the one managed by the Contentful library, and Next won’t have a hand in it. Additionally, you’ll benefit from the latency and availability of Contentful’s CDN.

Limit your usage.

It may be possible to avoid cycles based on your usage. If your API calls’ fetch depth  (“Link Level”) is shallow, or your content’s connections are predictable and limited, you might be okay. But I wouldn’t recommend accepting constraints here. I’d prefer to preserve flexibility for the authors developing content and the developers querying it.

Use the GraphQL API.

Contentful also offers a GraphQL API. Where the REST API requires you to specify the traversal depth once, upfront, GraphQL allows you to express the exact shape of the data you’re looking for. In Contentful’s words:

With GraphQL you specify the equivalent depth of the includes response through the construction of your query.

Opt out of built-in Resolution

That feature of the Contentful SDK where it reconstitutes the data structure for you? Contentful calls it Link Resolution, and you can turn it off. In the v10 JavaScript library, it looks like this:

const entries = await client.withoutLinkResolution.getEntries();

This will produce serializable JSON, fit for use with Next.js.

And deal with it on the client

We have a couple of options to handle the unresolved Contentful data on the frontend:

  1. Manage it in the application code. Look up IDs, handle errors when they can’t be found, etc.
  2. Invoke Contentful’s resolution process explicitly during rendering. Link resolution is normally used from within Contentful’s JavaScript SDK, but the implementation is published as a separate package you can use directly:

import resolveResponse from "contentful-resolve-response";

// This is valid JSON, suitable for use with Next.js:
const serializableEntries = await client.withoutLinkResolution.getEntries();

// These are resolved Contentful entries with embedded Links and Assets:
const resolvedEntries = resolveResponse(serializableEntries);

By postponing link resolution until render time, you can again have the best of both worlds: serializable data over the wire, and a nice data structure for your client logic. It adds a bit more complexity to your app, but if you want to be able to server-render unpredictable Contentful data, I think it’s worth it.

Working Together

Contentful and Next.js each provide powerful features for the needs of their respective users. Just be mindful of how you connect them. With care, they can be made to mesh nicely.

If you’re looking at integrating Contentful with Next.js (or another server-rendered framework), I’d recommend spending some time with these Contentful posts:


Join the conversation

Your email address will not be published. Required fields are marked *