Rendering code blocks with Contentful, TypeScript, and Vue

After researching many CMS options for this blog, I chose Contentful for the huge flexibility provided by custom content models. It's easy enough, just setup a new Nuxt project (in this case using TypeScript), and install the Contentful client and renderer. Once you fetch all your blog post entries, you end up with something like this to render rich text:

import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
import { Document } from '@contentful/rich-text-types';

const props = defineProps<{
  content: Document,
}>();

const renderedContent = documentToHtmlString(props.content);
<template>
  <article>
    <!-- date, title, tags, etc. -->
    <div class="article__content" v-html="renderedContent" />
  </article>
</template>

This approach works, however I was disappointed at the lack of integrated code blocks. Initially, I tried to use the inline code feature, however each line of code is rendered separately and wrapped in <p> tags. This makes syntax highlighting difficult.

Luckily, Contentful supports embedded entries to model more complex data. First, create a new "Content model" to hold the contents of the code block and take note of the identifier. This approach also allows you to store a language for syntax highlighting and/or display.

Code block content model

Now, when you embed a code block it isn't rendered at all. Let's fix that. Just implement a renderNode function for nodes of type BLOCKS.EMBEDDED_ENTRY. Use your content model's identifier in place of codeBlock.

import { BLOCKS } from '@contentful/rich-text-types';

// ...

const renderedContent = documentToHtmlString(props.content, {
  renderNode: {
    [BLOCKS.EMBEDDED_ENTRY]: (node: any) => {
      const { sys, fields } = node.data.target;
      if (sys?.contentType?.sys?.id !== 'codeBlock') {
        return undefined;
      }

      return `<pre>${fields.code}</pre>`;
    },
  },
});

Pretty good, but let's add syntax highlighting. Just install highlight.js, then highlight the code block's contents inside of the renderNode function. Here's the final result:

import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
import { BLOCKS, Document } from '@contentful/rich-text-types';
import highlightjs from 'highlight.js';

const props = defineProps<{
  content: Document,
}>();

const renderedContent = documentToHtmlString(props.content, {
  renderNode: {
    [BLOCKS.EMBEDDED_ENTRY]: (node: any) => {
      const { sys, fields } = node.data.target;
      if (sys?.contentType?.sys?.id !== 'codeBlock') {
        return undefined;
      }

      const { value: code } = highlightjs.highlight(fields.code, {
        language: fields.lang,
      });

      return `<pre>${code}</pre>`;
    },
  },
});

The same technique can be used to implement quotes, summaries (see below), and other embedded content.

tl;dr

Create a content model to represent code block contents, then implement a renderNode function to highlight the code and render HTML.