• rich text
  • contentful

The power of the Contentful Rich Text field

Rich Text is a field type that enables authors to create rich text content, similar to traditional "What you see is what you get" (wysiwyg) editors. The key difference here is that the Contentful Rich Text field response is returned as pure JSON rather than HTML. Additionally, it allows entries and assets within our Contentful space to be linked dynamically and embedded within the flow of the text. It offers common text formatting options such as paragraphs, lists and all that good stuff, but allows us to embed and link other references, too.

Here is a video entry embedded as an entry in the Rich Text field editor.

Here is an image asset.

Blue and purple galaxy digital wallpaper

And here is a code block entry.

/*
 * Format a blog post published date for a human to read.
 * Output is e.g. 16 Feb 2020
 */

export function formatPublishedDateForDisplay(dateString) {
  const timestamp = Date.parse(dateString);
  const date = new Date(timestamp);
  return `${date.getDate()} ${getMonthStringFromInt(
    date.getMonth(),
  )} ${date.getFullYear()}`;
}

A look at the code in this project

The Rich Text field is rendered via the RichTextPageContent component, which uses the Contentful package @contentful/rich-text-react-renderer.

The Rich Text field response (blogPost.body) contains the following two top-level nodes — json and links. json includes the Rich Text JSON tree representing whatever people put into the editor. It is to point out that this JSON structure only includes ids to possibly-linked references. These references can then be queried using the `links` node.

"body": {
  // JSON struction of the Rich Text field
  "json": {
    # ...
  }
  // all referenced assets/entries
  "links": {
    # ...
  }
}

Rendering the Rich Text response from GraphQL with linked assets and entries on the front end

We use `documentToReactComponents` from @contentful/rich-text-react-renderer to render our Rich Text field data to the DOM, whilst constructing an options object using a custom function to process a bit of logic to resolve our links.

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 access the data from the maps we created at the top of the function, and render it accordingly. 

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 entries as needed by looking at the __typename 
        // referenced in the GraphQL query
        if (entry.__typename === "CodeBlock") {
          return (
            <pre>
              <code>{entry.code}</code>
            </pre>
          );
        }

       if (entry.__typename === "VideoEmbed") {
         return (
            <iframe
              src={entry.embedUrl}
              height="100%"
              width="100%"
              frameBorder="0"
              scrolling="no"
              title={entry.title}
              allowFullScreen={true}
            />
          );
        }

      },
      [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} alt="My image alt text" />
        );
      },
    },
  };
}

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

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

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

Take a look at the full Contentful Rich Text documentation from Contentful here.

A headshot of Salma wearing black on a red patterned background.

Salma Alam-Naylor

✨⚡️ I help developers build stuff, learn things, and love what they do • I code live on Twitch • DevRel • She/Her ⚡️✨