Boring reasoning for this post alert: Recently I decided to start working on this blog again, which was originally just a Drupal site, then headless Drupal, and as I was working on revamping the front-end using VueJS, it occurred to me that I was spending far too much time tweaking for the API than I should in a headless/decoupled site, so I decided it was time for a change.
I decided to give Contentful a try since some of our clients had been inquiring and would give me an oppotunity to learn some new stuff.
The conversion from Axios + Drupal over to Contentful was actually surprisingly simple; however, as with most new systems Contentful was not without its headaches.
Rich Text Rendering
If you look at the docs, there are some handy bits about how to handle this as well as some nice SDKs, but I of course need some special stuff ...cause I'm special.
For all of these examples, I'm going to use the rich-text-html-renderer as well as the rich-text-types libraries:
javascript
import { documentToHtmlString } from "@contentful/rich-text-html-renderer"
import { MARKS, BLOCKS } from '@contentful/rich-text-types';
Assets:
For my images, I wanted to put the class img-fluid and mb-3 on them since I am using Bootstrap as well as give it some alt text.
Here is my function to handle image entities:
javascript
richText: function(text) {
const richTextOptions = {
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: asset => {
const { title, file } = asset.data.target.fields
const mimeType = file.contentType
const mimeGroup = mimeType.split('/')[0]
switch (mimeGroup) {
case 'image':
return `<img class="img-fluid mb-3" alt="${title || ''}" src="${file.url}" />`
}
},
}
}
return documentToHtmlString(text, richTextOptions)
}
Of course this can easily be extended for video or any other asset which is why I'm checking the mime group as that is exactly my plan :)
Code Blocks:
I'm using Highlight.js for my code snippets, so I needed to be able to output those into a compatible format. Unfortunetly Contenful only supports simplistic one-liners and splits multiple lines of "code" into different paragraphs.
So for simple code, I use the WYSIWYG and combined with the MARKS.CODE type, I can achieve what I want there:
javascript
renderMark: {
[MARKS.CODE]: text => `<span class="bg-info rounded p-1">${text}</span>`
},
Pretty straight-forward.
Now, for my larger code snippets I couldn't find a work-around for the paragraphs short of parsing it all which I didn't want to do, so instead I create a new Entity type called "Snippet" in Contentful, with a field for the type of snippet to pass as well:

Then I render it like so:
javascript
[BLOCKS.EMBEDDED_ENTRY]: node => {
const contentType = node.data.target.sys.contentType.sys.id
switch (contentType) {
// Contentful Snippet entity.
case 'snippet': {
let { type, snippet } = node.data.target.fields
// Escape HTML.
snippet = snippet.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
return `<pre><code class="${type}">${snippet}</code></pre>`
}
}
},
Pretty neat huh? This entire post contains all of the above elements.
Here's my full function (for now) if you'd like to rip it off you filthy thief:
javascript
richText: function(text) {
const richTextOptions = {
renderMark: {
[MARKS.CODE]: text => `<span class="bg-info rounded p-1">${text}</span>`
},
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: node => {
const contentType = node.data.target.sys.contentType.sys.id
switch (contentType) {
// Contentful Snippet entity.
case 'snippet': {
let { type, snippet } = node.data.target.fields
// Escape HTML.
snippet = snippet.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
return `<pre><code class="${type}">${snippet}</code></pre>`
}
}
},
[BLOCKS.EMBEDDED_ASSET]: asset => {
const { title, file } = asset.data.target.fields
const mimeType = file.contentType
const mimeGroup = mimeType.split('/')[0]
switch (mimeGroup) {
case 'image':
return `<img class="img-fluid mb-3" alt="${title || ''}" src="${file.url}" />`
}
},
}
}
return documentToHtmlString(text, richTextOptions)
}