/contentful-graphql-vs-rest

A look at the difference between how to fetch and parse linked assets and entires in the Rich Text field in Contentful.

Primary LanguageJavaScriptMIT LicenseMIT

Contentful GraphQL vs REST: fetching and rendering linked assets in the Rich Text field in React in two ways

If you're using the Contentful Rich Text field in your content model, use this example code to check out how you can render linked assets inside the Rich Text field using both the REST API and GraphQL API

⏭ Skip to REST API

⏭ Skip to GraphQL API


This is a Next.js project bootstrapped with create-next-app.

Getting Started

Fork this repository to your GitHub account, and clone it to your local machine using git or the GitHub CLI.

Install dependencies:

npm install
# or
yarn install

At the root of your project, create an .env.local file and copy the contents from .env.local.example. This will provide you with a connection to an example Contentful space. You can view the live example website for this space here.

Run the development server

npm run dev
# or
yarn dev

Making the API calls

Example code for the GraphQL API can be found in pages/graphql.

Example code for the REST API can be found in pages/rest.

API calls are executed at build time in getStaticProps() on each page. Read more about getStaticProps() in Next.js.

Both code examples use @contentful/rich-text-react-renderer to render the Rich Text field nodes to React components.

REST API

The code for the REST API uses the JavaScript Contentful SDK:

import { createClient } from "contentful";

REST API: Fetching the data

export async function getStaticProps() {
  // Create the client with credentials
  const client = createClient({
    space: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID,
    accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN,
  });

  // Query the client with the following options
  const query = 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);

  // As we are using getEntries we will receive an array
  // The first item in the items array is passed to the page props
  // as a post
  return {
    props: {
      post: query.items[0],
    },
  };
}

REST API: Rendering the Rich Text field

import { documentToReactComponents } from "@contentful/rich-text-react-renderer";
import { BLOCKS } from "@contentful/rich-text-types";

// Create a bespoke renderOptions object to target BLOCKS.EMBEDDED_ENTRY
// (linked entries e.g. code blocks)
// and BLOCKS.EMBEDDED_ASSET (linked assets e.g. images)

const renderOptions = {
  // other options...
  renderNode: {
    // other options...

    [BLOCKS.EMBEDDED_ENTRY]: (node, children) => {
      // target the contentType of the EMBEDDED_ENTRY to display as you need
      if (node.data.target.sys.contentType.sys.id === "codeBlock") {
        return (
          <pre>
            <code>{node.data.target.fields.code}</code>
          </pre>
        );
      }
    },
    [BLOCKS.EMBEDDED_ASSET]: (node, children) => {
      // render the EMBEDDED_ASSET as you need
      return (
        <img
          src={`https://${node.data.target.fields.file.url}`}
          height={node.data.target.fields.file.details.image.height}
          width={node.data.target.fields.file.details.image.width}
          alt={node.data.target.fields.description}
        />
      );
    },
  },
};

// Render post.fields.body to the DOM using
// documentToReactComponents from "@contentful/rich-text-react-renderer"

export default function Rest(props) {
  const { post } = props;

  return <main>{documentToReactComponents(post.fields.body, renderOptions)}</main>;
}

GraphQL API

The code for the GraphQL API uses the Fetch API with no other dependency packages.

GraphQL API: Fetching the data

/**
 * Construct the GraphQL query
 * Define all fields you want to query on the content type
 *
 * IMPORTANT:
 * `body.json` returns a node list (e.g. paragraphs, headings) that also includes REFERENCE
 * nodes to assets and entries.
 *
 * These reference nodes will not be returned with the full data set included from the GraphQL API.
 * To ensure you query the full asset/entry data, ensure you include the fields you want on the
 * content types for the linked entries and assets under body.links.entries, and body.links.assets.
 *
 * The example below shows how to query body.links.entries and body.links.assets for this particular
 * content model.
 */

export async function getStaticProps() {
  const query = `{
    blogPostCollection(limit: 1, where: {slug: "the-power-of-the-contentful-rich-text-field"}) {
      items {
        date
        title
        slug
        body {
          json
          links {
            entries {
              block {
                sys {
                  id
                }
                __typename
                ... on CodeBlock {
                  description
                  language
                  code
                }
              }
            }
            assets {
              block {
                sys {
                  id
                }
                url
                title
                width
                height
                description
              }
            }
          }
        }
      }
    }
  }`;

  // Construct the fetch options
  const fetchUrl = `https://graphql.contentful.com/content/v1/spaces/${process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID}`;

  const fetchOptions = {
    spaceID: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID,
    accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN,
    endpoint: fetchUrl,
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ query }),
  };

  // Make a call to fetch the data
  const response = await fetch(fetchUrl, fetchOptions).then((response) => response.json());
  const post = response.data.blogPostCollection.items ? response.data.blogPostCollection.items : [];

  // Return the post to the page props
  return {
    props: {
      post: post.pop(),
    },
  };
}

GraphQL API: Rendering the Rich Text field

The key difference in the GraphQL API compared to the REST API is that linked entry nodes are available under body.links as opposed to their data being returned in line with with the other body nodes.

In order to target asset and entry data when rendering BLOCKS.EMBEDDED_ENTRY and BLOCKS.EMBEDDED_ASSET with documentToReactComponents, we can create an assetBlockMap (id: asset) and entryBlockMap (id: entry) to store data we can reference by ID.

When the renderOptions reaches the entry and asset types, we can fetch the data from the maps we created at the top of the function, and render it accordingly.

Note that the second parameter of documentToReactComponents is now a function, compared to an object in the REST example.

import { documentToReactComponents } from "@contentful/rich-text-react-renderer";
import { BLOCKS } from "@contentful/rich-text-types";

// Create a bespoke renderOptions object to target BLOCKS.EMBEDDED_ENTRY (linked entries e.g. code blocks)
// and BLOCKS.EMBEDDED_ASSET (linked assets e.g. images)

function renderOptions(links) {
  // create an asset block map
  const assetBlockMap = new Map();
  // loop through the assets and add them to the map
  for (const asset of links.assets.block) {
    assetBlockMap.set(asset.sys.id, asset);
  }

  // create an entry block map
  const entryBlockMap = new Map();
  // loop through the assets and add them to the map
  for (const entry of links.entries.block) {
    entryBlockMap.set(entry.sys.id, entry);
  }

  return {
    // other options...

    renderNode: {
      // other options...

      [BLOCKS.EMBEDDED_ENTRY]: (node, children) => {
        // find the entry in the entryBlockMap by ID
        const entry = entryBlockMap.get(node.data.target.sys.id);

        // render the entry as needed
        if (entry.__typename === "CodeBlock") {
          return (
            <pre>
              <code>{entry.code}</code>
            </pre>
          );
        }
      },
      [BLOCKS.EMBEDDED_ASSET]: (node, next) => {
        // find the asset in the assetBlockMap by ID
        const asset = assetBlockMap.get(node.data.target.sys.id);

        // render the asset accordingly
        return (
          <img src={asset.url} height={asset.height} width={asset.width} alt={asset.description} />
        );
      },
    },
  };
}

// Render post.body.json to the DOM using
// documentToReactComponents from "@contentful/rich-text-react-renderer"

export default function GraphQL(props) {
  const { post } = props;

  return <main>{documentToReactComponents(post.body.json, renderOptions(post.body.links))}</main>;
}