Contentful is a headless content management system better referred to as a content platform. As a content platform, it offers users the opportunity to organize and structure content within Contentful itself. The backbone is a “content model” created by a user which includes a collection of content types and the relationships between those content types. In turn, a content type consists of fields: text fields, fields for assets like images or videos, time and date fields, fields that reference entries from other content types, and more.
One of the fields you can include in a content type is a rich text field. Rich text fields are similar to WYSIWYG editors where “what you get” includes essentials like italicizing text, embedding images, and adding inline code.
But there’s one relatively basic thing that Contentful just doesn’t offer in its rich text editor: strikethrough text.
We’re in the process of migrating The Foundry (the very place where you’re reading this article!) from ExpressionEngine to Contentful. Although ExpressionEngine has served us well over the years, we’re excited to transition from a traditional CMS to Contentful’s content platform approach with the powerful content authoring experience it delivers. We had to make a lot of decisions during this migration like the way that we input content (like the content of this article). In ExpressionEngine, we use a mix of markdown and HTML to create content, but our Contentful fields will primarily use rich text instead.
Rich Text in Contentful: Powerful but with Limitations
Rich text in Contentful is pretty powerful. It has standard formatting tools, like bold text, lists, and tables. It also has the ability to embed entries from other content types directly into your rich text. Users at the administrator level can even enable or disable these tools as necessary—which is pretty cool.
Despite having all of these tools at our disposal, we still had to do a bit of brainstorming as to how to include strikethrough text in rich text entries.
Option 1: Embed Content Types
One potential solution is creating a new content type exclusively for strikethrough text. We could enable embedding that content type into the rich text editor of our articles so content authors could create new (or select existing) strikethrough text entries for insertion.
This is the solution we decided on for the sake of our migration efforts. Creating a new content type was a straightforward way for developers to render strikethrough text. However, once we move beyond migration and into the stage where content authors directly enter content into Contentful, this may need an update. Why? Creating a new entry to represent each instance of strikethrough text could become a nuisance for content authors. Although strikethrough text isn’t used quite as commonly as something like bold or italics, it’d still be cumbersome to go through the entry creation process for each individual bit of strikethrough text you wanted to create. Every time an author wanted a bit of text to appear with a strikethrough, they would need to create a new entry, edit that entry to contain the text of the strikethrough, and then embed that entry. This process distracts authors from the entry they originally started writing. Additionally, bits of strikethrough text are unlikely to be used consistently in multiple places, so authors really would have to go through the content creation process each and every time.
Option 2: Reappropriate an Unused Formatting Tool
Another potential solution could be hijacking an existing rich text tool that we aren’t using. For example, we don’t underline text on The Foundry because we don’t want readers to confuse underlined text with links. For example, we have disabled underlined text in the rich text editor because we don’t use it on The Foundry. We could re-enable underlined text but render it as strikethrough text instead.
When we’re using Contentful with React.js, we render rich text by invoking the documentToReactComponents()
function Contentful provides in its rich-text-react-renderer package. The first argument of this function is the rich text document itself. The second argument isn’t required but offers options for rendering the rich text document.
We could pass in an options object that looks something like this and achieve our strikethrough dreams:
const options = {
renderMark: {
[MARKS.UNDERLINE]: (text) => <s>{text}</s>
},
};
As simple as that solution would be for a developer, it’s pretty confusing for a content author. Every time you want to include strikethrough text, they would see it appear in the rich text editor as underlined text. Sure, the preview environment might clear up the confusion, but it still feels like a pesky, misleading redirection.
Option 3: Keep the Markdown
A third potential solution appeared next: why not keep things the way that they are? Could we nix most markdown but maintain the bits that we needed for strikethrough text? Was it possible to integrate some (but not all) markdown capabilities inside of our rich text editor?
(The answer is yes–and without any regular expressions even!)
This approach does soften Contentful’s strength of providing rich text as a JSON object as opposed to a blob of HTML and makes our content rely slightly more on context. However, a little behind-the-scenes work from developers can mitigate that impact. Although developers would need to work slightly harder than in the previous two solutions to implement strikethrough text inside of the rich text editor, the solution wouldn’t require any sort of large overhaul or the need for developers to input content for authors.
But this solution would take some additional steps.
Modifying How We Render Rich Text
The official Contentful blog gave us a nudge in the right direction with their article “Rich Text field tips and tricks from the Contentful DevRel team.” They offer a solution for rendering line breaks in rich text that we figured was as good a starting point as any. The renderText()
function they provide leverages their rich-text-react-renderer package and takes advantage of the aforementioned options
object that you can pass in when rendering rich text.
const renderOptions = {
renderText: text => {
return text.split('\n').reduce((children, textSegment, index) => {
return [...children, index > 0 && <br key={index} />, textSegment];
}, []);
},
};
This function creates an array out of the text and splits it into different values within the array depending on where there’s markdown for line breaks. If we’re not looking at the first item in our array, we return a line break and then our current text value.
Cool! If we just shuffle some stuff around, that ought to fix the problem, right?
Wrong. After all, line breaks and text formatting are quintessentially different. Line breaks are self-closing elements. They go alongside text without really interacting with it. On the other hand, strikethrough elements are not self-closing and need to envelop text to be meaningful. To successfully insert a line break, all it needs to know is if there’s a new line character present. If it is, then add a <br />
tag.
Meanwhile, strikethroughs are a little more complicated. For example, if a paragraph only has some strikethrough text, then opening and closing the strikethrough tags in the correct location is important. Each strikethrough means something different in relation to the strikethrough that (may have) preceded it. A line break is a line break, but a strikethrough tag may be the start of strikethrough text (i.e. <s>
) or the end of strikethrough text (i.e. </s>
). Strikethrough tags must occur in pairs and should “hug” text.
With those requirements in mind, let’s create our strikethrough function.
Creating a Function to Render Markdown
This is going to get a bit messier than the function for rendering line breaks, so let’s pull this out into a new function. We’re going to touch on types a bit over the course of this article, so the examples will be in TypeScript. Our fresh start looks something like this:
const renderStrikethrough = (text: string): string => text;
In order for this to run when our rich text renders, we’ll reference our new renderStrikethrough()
function in a hollowed-out version of the renderOptions()
function.
const renderOptions = {
renderText: (text: string): string => renderStrikethrough(text)
};
Now our renderStrikethrough()
function will run for each rich text node we receive from Contentful. Paragraphs are nodes, but so are things like inline hyperlinks or formatting like bold text. Not every node we render will include strikethrough text, so we’ll need to check for that beforehand. Let’s add a condition to our renderText()
function that checks to see if we need to run our renderStrikethrough()
function.
const renderOptions = {
renderText: (text: string): string => {
const hasStrikethrough = text.includes('~~');
if (hasStrikethrough) return renderStrikethrough(text);
return text;
},
};
Now that we’ve got some space carved out for where text that contains strikethrough text will go, let’s work on replacing our markdown characters () with actual strikethrough tags. Because we’re going to be working with JSX elements, we’ll ultimately need to return an array of both strings and React nodes This allows us to return both plain text (as strings) and strikethrough text inside of strikethrough tags (as JSX elements). Otherwise, we’ll wind up returning a string interpolated with strikethrough tags when we want to return a functioning element with strikethrough text.
We can kill two birds with one stone with the split()
method. If we pass our markdown characters in as an argument, it will create an array based on our text, separated by where our strikethrough characters were.
const renderStrikethrough = (text: string): string[] | React.ReactNode[] => text.split('~~');
For the sake of consistency, let’s return our text that doesn’t include strikethrough marks as an array as well. Our renderOptions()
function will now always return an array made up of either strings or React nodes.
const renderOptions = {
renderText: (text: string): string[] | React.ReactNode[] => {
const hasStrikethrough = text.includes('~~');
if (hasStrikethrough) return renderStrikethrough(text);
return [text];
},
};
We’re headed in the right direction now that we’ve removed our markdown characters. Next, we’ll want to intersperse some strikethrough tags. Before we get around to that though, let’s think about what our text arrays could look like.
If a string begins with strikethrough text, we’d get an array that looks something like this: [‘‘, ‘the text we want to see with a strikethrough’, ‘other text without a strikethrough’, ‘more strikethrough text’, ‘other text’]
Even if we have a string that begins with a whitespace character (before getting to the strikethrough character), our array would still look more or less the same: [’ ‘, ‘the text we want to see with a strikethrough’, ‘other text without a strikethrough’, ‘more strikethrough text’, ‘other text’]
Alternatively, if a string does not begin with a strikethrough, it would look something like this: [‘text without strikethrough’, ‘strikethrough text’, ‘other text without a strikethrough’]
This means that no matter what our string looks like, the zeroth item will never need to be wrapped in a strikethrough tag. However, the first item (i.e. the item after item zero in our zero-based array) will need to be wrapped in a strikethrough tag. Likewise, we will not wrap the second item in a strikethrough tag, but we will want to wrap the third item in a strikethrough tag. And there we have it: a pattern. We won’t need strikethrough tags for items with even indices (including zero), but we will need strikethrough tags for items in our array with odd indices.
We can now update our function to map over our split text and return plain text for odd indices and text in strikethrough tags otherwise.
const renderStrikethrough = (text: string): string[] | React.ReactNode[] => {
return text.split('~~').map((val, i) => {
return i % 2 === 0 ? val : <s key={i}>{val}</s>
}
);
};
Works like a charm! Now let’s break it.
Using Multiple Functions to Modify Rich Text
After all, what if we still want to render line breaks? Let’s take what the Contentful blog gave us to spin up a renderLineBreak()
function and then add it to our renderOptions
object.
const renderLineBreak = (text: string): string[] | React.ReactNode[] => {
return text.split('\n').reduce((children: any, textSegment, index) => {
return [...children, index > 0 && <br key={index} />, textSegment];
}, []);
};
const renderOptions = {
renderText: (text: string): string[] | React.ReactNode[] => {
const hasStrikethrough = text.includes('~~');
const hasLineBreak = text.includes('\n');
if (hasStrikethrough) return renderStrikethrough(text);
if (hasLineBreak) return renderLineBreak(text);
return [text];
},
};
But now something strange is happening. If we have both a line break and a strikethrough in the same paragraph, only our strikethrough renders. If we reverse the order of how we render strikethroughs and line breaks, then we see the same thing happen in reverse: our line breaks render but not our strikethroughs.
This is because the renderText()
function in Contentful is one of three functions that we can pass in as an option to our renderOptions
object. The two additional functions that we’re not using at this time are renderNode()
and renderMark()
. We used renderMark()
previously in this article to show how we could adjust the way we render an underline. We could also use this for things like rendering italics or inline code.
On the other hand, renderNode()
, can adjust how we render, well, nodes. In Contentful, nodes include things like headings, paragraphs, and list items. These things typically correspond to block-level elements. On the other hand, our renderText()
function corresponds to the text that makes up these nodes. So, we call our renderText()
function on a node-by-node basis.
We’re not rendering all of our strikethroughs and line breaks simultaneously right now because we’re only calling one of these functions once per node. To get all of our strikethroughs and line breaks, we’ll need to make sure we call both functions on each node.
We can do this with a bit of recursion to check if our strikethroughs have line breaks and if our line breaks have strikethroughs.
Let’s break the logic of our renderText()
function out into a separate function called renderMarkdown()
.
const renderMarkdown = (text: string): string[] | React.ReactNode[] => {
const hasStrikethrough = text.includes('~~');
const hasLineBreak = text.includes('\n');
if (hasStrikethrough) return renderStrikethrough(text);
if (hasLineBreak) return renderLineBreak(text);
return [text];
}
const renderOptions = {
renderText: (text: string): string[] | React.ReactNode[] => renderMarkdown(text)
};
Next, we’ll call this renderMarkdown function on the strings inside of our renderStrikethrough()
and renderLineBreak()
functions.
For renderStrikethrough()
, this means calling renderMarkdown()
on returned values.
return i % 2 === 0 ? renderMarkdown(val) : <s key={i}>{renderMarkdown(val)}</s>;
For renderLineBreak()
, this means calling renderMarkdown()
when we return our current value.
return [...children, index > 0 && <br key={index} />, renderMarkdown(textSegment)];
And now both our strikethroughs and our line breaks render, even when they’re part of the same node.
Conclusion
Our renderMarkdown()
function takes us beyond what Contentful directly offers us and empowers us to render both strikethrough text and line breaks. The limitations of Contentful’s rich text editor don’t have to be limitations at all–if you’re willing to do a bit of work behind the scenes.