Rendering Contentful Rich Code Snippets in Gatsby

Hero image for Rendering Contentful Rich Code Snippets in Gatsby. Image by Iker Urteaga.
Hero image for '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/richtexttypes to define how each node type is displayed, and then pass that into @contentful/richtextreactrenderer 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

richtexttypes 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:

Paragraph nodes of this type will only have a single content node (conversely: normal paragraphs of text can have several if they have inline marks within them), and the marks field for the first content element with be type: 'code'. With this in mind, it is possible to output code blocks inside <pre> (and <code> if you like), whilst also outputting normal paragraphs wrapped in <p>:

[BLOCKS.PARAGRAPH]: (node, children) => {  if (    node.content.length === 1 &&    find(node.content[0].marks, { 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 whitespace: prewrap 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.

Essentially, the object that Contentful provides needs to be transformed so that adjacent, codeonly, paragraph nodes are combined together into a single node, with each separated by /n/n.

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

const mergeCodeBlocks = data => {  const mergedData = [];  const checkForType = arrayToCheck =>    arrayToCheck?.content?.length === 1 &&    arrayToCheck?.content[0]?.marks?.length &&    arrayToCheck?.content[0]?.marks[0]?.type === 'code';  for (let i = 0; i < data.length; i++) {    const current = data[i];    if (checkForType(current)) {      const codeBlock = current;      let j = i;      while (data[j + 1] && checkForType(data[j + 1])) {        codeBlock.content[0].value =           `${codeBlock.content[0].value}\n\n${data[j + 1].content[0].value}`;        j++;      }      mergedData.push(codeBlock);      i = j;    } else {      mergedData.push(current);    }  }  return mergedData;};

If you pass the data through that before passing it on to documentToReactComponents() and using the renderMark configuration we talked about earlier then your blocklevel code snippets will come through as single items rather than split out over multiple 'paragraph' fragments.

One thing to bear in mind: this is relatively intense data manipulation. You should make sure that your React component does not remanipulate the same data again every time it updates, especially as this could in theory result in your content becoming altogether malformed. There are any number of different ways of achieving this. Personally, I use an Effect Hook to manipulate the data once, and then store that in state for future updates.


Categories:

  1. CMS
  2. Contentful
  3. CSS
  4. Development
  5. Front‑End Development
  6. Gatsby
  7. Guides
  8. JavaScript
  9. JSX
  10. React
  11. Sass