Rendering Contentful Rich Code Snippets in Gatsby

Abstract image used to represent Rendering Contentful Rich Code Snippets in Gatsby
Image by Iker Urteaga.

Gatsby and Contentful make for an incredibly powerful combination: all the benefits of an extremely fast static site, built on a modern framework, with a CMS which provides a very generous free tier (although you could argue that the prices from thereon up are very steep), and an interface which clients quickly adapt to. There is even an official source plugin (gatsbysourcecontentful) to make connecting to, and reading from, Contentful all the more straightforward.

It is the same Gatsby and Contentful pattern I used on IMG Licensing, Wreel Agency, Red Central, and ToyBoxX, where a static front end and a headless CMS gave clients editorial control without sacrificing performance.

However, for techtype bloggers and writers, there is a problem with the way that Contentful's rich text outputs code blocks, and it has gone unresolved since 2019. Namely that it is not straightforward to render blocks or snippets of code from within Contentful rich text.

At a high level: rich content comes through as an object. Each node within the object corresponds to a block of copy (a paragraph, a title, an image, etc). When implementing a component to render these, you use @contentful/rich-text-types to define how each node type is displayed, and then pass that into @contentful/rich-text-react-renderer to render the result into React.

For more a detailed overview of how these two technologies come together, you can read Contentful's own excellent article here. This is exactly how the content here on my personal website and blog works.


Inline Code

rich-text-types breaks down the content into:

  • BLOCKS: essentially blocklevel elements: paragraphs, headings, lists, embedded assets (images), etc;
  • INLINES: primarily these are just hyperlinks although you can also have inline embedded assets;
  • MARKS: these are inline text formats: bold, italic, underlined, and of most interest to us in this article code.

To wrap these different content types, we simply use renderMark to determine how each is handled, for example:

renderMark: {
  [MARKS.BOLD]: text => <b>{text}</b>,
  [MARKS.ITALIC]: text => <i>{text}</i>,
  [MARKS.UNDERLINE]: text => <u>{text}</u>,
  [MARKS.CODE]: text => <code>{text}</code>,
}

I like to add a check here to make sure that there is actually content within each before rendering so that we don't end up with empty elements on the page:

[MARKS.CODE]: text => {
  if (text.length > 0) {
    return <code>{text}</code>;
  }
}

This handles inline code snippets: those which appear within the flow of a paragraph. Just like this.

Photograph of Benny the blue LEGO spaceman, standing on top of a Nintendo console by Hello I'm Nik on Unsplash.

Actual Blocks of Code

What this does not do is provision for larger, standalone blocks of code, as you will see featured frequently within my blog. As I mentioned previously Contentful breaks the rich text object into nodes that roughly correlate to blocklevel elements, but they do not provide a 'code' BLOCK type.

What this means is that when you create a code block in Contentful, what you are actually doing is creating paragraphs, and then embedding a single 'code' mark within it. In our renderer it is relatively straightforward to detect these:

For this convention, check that the node is a paragraph containing exactly one text node, and that the text has a code mark somewhere in its marks array. The order of marks is not significant. That lets us render the code as <pre><code> whilst leaving ordinary paragraphs as <p>:

[BLOCKS.PARAGRAPH]: (node, children) => {
  if (
    node.content.length === 1 &&
    node.content[0].nodeType === 'text' &&
    node.content[0].marks.some(mark => mark.type === 'code')
  ) {
    return <pre><code>{node.content[0].value}</code></pre>;
  }
  return <p>{children}</p>;
}

Adjacent Blocks of Code

This will then get you almost the entire way there. The only issue is that because Contentful splits code blocks up essentially by 'paragraph', if you have a large code block in your content with line breaks, what you will end up with is multiple, adjacent <pre> tags in the output even though from within Contentful these appear as part of the same single code block.

This may not be an issue for you at all depending on how you choose to lay out your page, but for me it left gaps in between each, breaking up the code block into sections. For example, a screenshot of a single code block from a previous article with lime outlining around each <pre> tag to show the separation:

A screenshot showing adjacent <pre> code tags output from Contentful from the same single code block.

Fortunately, you can work around this with a little CSS and the adjacent sibling combinator to remove the top padding and pull the adjacent blocks upward to fill the empty gap:

.rich-code {
  $margin--small: 3rem;
  margin: (1.5 * $margin--small) (-1 * $margin--small);
  padding: $margin--small;

  & + & {
    margin-top: (-2 * $margin--small);
    padding-top: 0;
  }
}

I am by no means a fan of negative margins, but in this instance, it does the trick just fine:

A screenshot showing adjacent <pre> code tags output from Contentful from the same single code block, using CSS to close up the gap between each one.

At this point, you may already have the solution you need. For me, this was a compromise that worked: it visually looked ok, but it resulted in nonsemantic markup, and individual sections within the code block would wrap at different points on narrower screens. I had to use white-space: pre-wrap to combat this, but could not do anything more interesting with my code, such as syntaxhighlight it or allowing horizontal scrolling on small screens.


Combining Adjacent Snippets of Code

The final piece of this puzzle is relatively straightforward, but eventually needed the datatransformation wizardry of my good (and very talented) friend Ben Stokoe to implement.

The transformation combines adjacent codeonly paragraphs in the content array, separating their text with \n\n. It must check the node types as well as the marks, so a codemarked heading or an inline link is not mistaken for a code paragraph.

The final transformation code does exactly that, and looks like this:

const cloneJson = value => {
  if (Array.isArray(value)) return value.map(cloneJson);
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value).map(([key, child]) => [key, cloneJson(child)])
    );
  }
  return value;
};

const isCodeParagraph = node =>
  node?.nodeType === 'paragraph' &&
  node.content?.length === 1 &&
  node.content[0].nodeType === 'text' &&
  node.content[0].marks?.some(mark => mark.type === 'code');

const mergeCodeBlocks = data => {
  const mergedData = [];

  for (const original of data) {
    const current = cloneJson(original);
    const previous = mergedData[mergedData.length - 1];
    if (isCodeParagraph(previous) && isCodeParagraph(current)) {
      previous.content[0].value += '\n\n' + current.content[0].value;
    } else {
      mergedData.push(current);
    }
  }
  return mergedData;
};

Pass the content array through mergeCodeBlocks() before giving the document to documentToReactComponents(). Use the paragraph and mark renderers above. The input stays unchanged, and adjacent code paragraphs become one block in the rendered output.

Run the same pure transformation for server rendering and the first browser render. It does not need an Effect or a second state copy: doing the work only after mounting would leave the server output untransformed and change the content after hydration. If profiling shows a meaningful cost, cache the transformed result for an unchanged input.

Postscript

June 2026: this is a GatsbyandContentful implementation note from a stack I used on several static projects. The richtext problem is still recognisable, but for current CMS work I would handle it as part of a broader headless CMS integration or migration review.

Want to find out more?

If you need senior handson support with a complex React or Next.js platform, migration, performance issue, or technical SEO problem, send me the context and I'll tell you where I can help.