Get traction with interaction: modeling interactive content with React and Contentful's rich text editor

Blog traction with interaction 01

A year ago, Clue relaunched its website as an encyclopedia for women and people with cycles. Clue's first product is a period tracker app, which is used by over 10 million people to track their symptoms and learn about patterns in their reproductive health. With the website relaunch, we put all of our content together in one organized place where people could more easily get the information they want.

Here’s an example of some of our encyclopedic content. This is one of our most popular articles, which explains what the significance of period blood color is. (Spoiler alert: it’s not that significant.)

We have a ton of articles like these. Lately, we’ve been thinking that our readers might be better served by interactive content that does more showing and less telling.

Another of our top 10 most popular articles is titled, “What is the clitoris?”. The article goes into detail about the anatomical structure of the clitoris, but the entire article was just text. We thought we could do better and asked:

What if, as you scroll through and read about the different parts of the clitoris, an illustration would highlight the parts being discussed so you could really get what you're reading about?


In our Contentful setup, we have a content type called “Article” with a Markdown body field. This field contains a bunch of text in the clitoris article, but we wanted to figure out how to get a plain text field to also include interactivity.

Our first attempt at this was to extend Markdown's syntax using the concept of "shortcodes." Basically, this means inserting an HTML comment in the middle of the article body, with special syntax inside of it that refers to an interactive piece. Our Markdown renderer would pick up that HTML comment and render a custom React component in its place. That way, writers could insert some custom interactive widget in the middle of an article, and control where it appears based on where they inserted the HTML comment.


But that didn't work super well, for several reasons:

  1. It was brittle. Our writers had to insert an HTML comment in a very specific format that could be easily broken
  2. The text in the interactive portion was hard-coded. Our writers would have to email me if they wanted to change that text.
  3. Because the text was hard-coded, any changes to the text required a code deploy

We knew we could do better. Specifically, we wanted to achieve the following goals with our interactive pieces:

  1. Writers should be able to make changes to text without waiting for a code deploy
  2. Content should be translatable using Contentful's existing localization functionality
  3. The setup shouldn’t be brittle
    • We wanted to avoid telling writers, "Make sure you include these items in this specific order!" since that means things could break easily.
    • If a piece has inline HTML to make the interaction work, and a writer accidentally deletes part of an HTML tag while making edits, the whole interactive piece could break
  4. Writers shouldn’t have to deal with code. Ideally, the interface should be very intuitive and easy-to-use for non-tech folks.
  5. Writers should be able to preview their changes and get quick feedback

To solve this problem, I turned to the New York Times. Their engineering blog contains a fascinating article about their rich text editor, which treats every bit of content in their articles as a series of nodes. Paragraphs, pull quotes, images, captions, headings — everything is just a node in their text editor. And then on the front end they can render different node types however they’d like.

I connected with an engineer there, and he said that for the interactive pieces, they simply created a new node type: an “interactive” node, which had a field that referenced the interactive code written for that specific article. When authoring an article they would create a new node, just like a paragraph tag or a header, and insert it into the middle of the article. Then, their renderer for the node would render the interactive content designed for it.


Contentful has a new rich text feature that works very similarly to the way the New York Times text editor works. On the left of the screenshot above, in the Contentful web app, you see a WYSIWYG text editor where you can compose rich text with formatting and so forth, without having to deal with the special syntax required by Markdown.

When you request rich-text content from the Contentful API, it represents your text as a series of blocks in the form of JSON objects that can be nested inside of each other. So a paragraph, a heading, a quotation — each of these are essentially a "node" in this giant JSON structure describing your content.

This allows for a great degree of flexibility. So instead of just composing text, you can also insert entries from other content types, right in the middle of the text!


You can see here in the screenshot how a content type called "Block: Video", has been inserted directly into the rich text body.

I realized I could create custom content types for any kind of interactive or custom content that I would need in an article, and then write React components that would render these content types exactly the way I want.


Next, I'll take you through the model we set up. First, I created a new content type and called it "Custom Block." It only has three fields: the “Name” field references an actual React component name — that is, the machine name of the React component. Microcopies is a multi-reference field to the "Microcopy" content type, which I'll get to in a second. And “Asset(s)” is for all the assets the component needs, with the title of each asset set to a machine name that the custom block refers to.


The Microcopy content type is simply a key-value pair. The key is a machine name referring to this piece of copy, and the value is a long text field, which can even include Markdown. This article was my inspiration for microcopy — give it a read!

So, we have a custom block model, and we can insert it into the middle of an article's rich text field. Let's dive into the code and see what it takes to render all of this.

First, we'll install the rich-text-to-react NPM module. This module was specifically made for Contentful's rich text feature. It recursively loops over the JSON object and creates a React component tree for the whole thing.


It also lets you override its default renderers for different node types.


For example, if you want to use styled components to make a styled paragraph tag, you can pass that styled component in the in the config, and it will use that rather than a native paragraph tag.

As you can see, we have styled versions of many of the node types that you'll find in our rich text documents. But those first three renderers (lines 26-28) are the more interesting ones.


Line 26 specifies the renderer for embedded entries. The renderer itself checks the content type of the embedded entry, and then delegates to the React component that knows how to render it. Meaning that if it's a custom block entry, it will delegate to the CustomBlock React component.


Here's my CustomBlock component. It doesn't do a lot on its own — it just imports a hard-coded registry of all the different custom blocks I've created. Then it looks in that registry for a component with the name we specified in the custom block's "Name" field, and renders it if it exists.


Here's the registry itself.

This maps machine names of custom blocks to an asynchronously loaded component. We use code splitting here so that the custom blocks aren't loaded as part of the main JavaScript bundle.

As I mentioned earlier, we grab a React component from the custom block registry based on its name, and then render it.


You'll notice that we're also passing the entire custom block entry from Contentful into the component we loaded from the registry. This gives the component access to all of the microcopies and assets that we've entered in Contentful. Then, that component uses the machine names of the microcopies to determine which pieces of text should show up where.


I won't go into too much of our code, but here's an example from the clitoris article of how the microcopy can be used. You can see how the alt text of several images is set via this microcopies object, which is just an object of key-value pairs generated from the microcopies array in Contentful.

So, to recap:

  1. First, the rich-text-to-react module goes through the rich text tree, and converts nodes to their corresponding React components
  2. For nodes that are embedded custom blocks, the CustomBlock React component is loaded and rendered
  3. The CustomBlock component takes the name of the custom block entry, and looks it up in its hard-coded registry of custom blocks. If it finds it, it renders the component linked in the registry.


Moving on: let's actually go through the process of creating an article with this setup. As you remember, here's the old way of doing it: a Markdown body field with an HTML shortcode in the middle of it.

contentful-markdown-body contentful-rich-text-body

Compare the old process to how it looks as rich text: With rich text, we have the WYSIWYG editor; but more importantly…

contentful-markdown-body-shortcode contentful-rich-text-body-custom-block

…we can embed our custom block entry right in the middle of the text. Much better than the HTML shortcode on the left.

Let's click the edit button and see what the custom block record looks like.


Now we see all the microcopies associated with this article. Let's go one level deeper and click the edit button on one of those.


Now you can you see the alt text for a single image.

Once we've set up all the microcopies in the custom block, the article is ready to go. We now have a custom block in the middle of the article. The parts of the text it's scrolling past are actually microcopies that, as they go past, trigger the animation at the top to switch.

If you want to see a couple more examples of custom blocks in the middle of rich text fields, visit our jobs page (we’re hiring, by the way) and our app download page, which both include more interactivity and animations.

Earlier I mentioned the goals that I had when implementing interactivity in rich text. Now is probably a good time to revisit them and see how we did:

  1. Writers can make changes to text without waiting for a code deploy
    • ✅ Check! They can make changes via the microcopies
  2. Content can be translated using Contentful's existing localization functionality
    • ✅ Also check! Microcopies can be localized
  3. The setup isn't brittle
    • 👌 Mostly good, although there’s still a risk that writers could accidentally change one of the keys of the microcopies. Ideally, they shouldn't be able to edit the keys at all — just the values. But we're still in better shape than before, when I was using HTML tags and comments.
  4. Writers don't have to deal with code
    • 👌 Again, mostly good. I do think of machine names as being a bit code-ish, but other than that, they can just insert custom blocks into text using the native Contentful interface, and don't have to worry about using shortcode syntax.
  5. Writers are able to preview their changes and get quick feedback.
    • ✅ Check! Writers can edit the microcopies and then use Contentful's content preview to see how their changes look in staging.

Overall, I'd say we did pretty well.

That said, I do see some clear areas for improvement.

First, it'd be great to have a clearer UI for editing microcopy. Contentful does have a feature for bulk editing referenced entries, but even that isn't exactly what I want, since writers can still edit the Key field, which is a bit risky. So I might want to develop a UI extension for editing microcopies.

Second, while the microcopies are indeed translatable, sending individual microcopies to translators and then copy-pasting them back in is obviously pretty tedious. So I'd like to set up some tooling that can, for example, auto-generate a JSON file that our writers can use to upload to our translation provider.

Lastly, I'd love to hear from readers on this solution and what could be better. Feel free reach out to me on Twitter: @jessepinho.

Updates in your inbox

Subscribe to receive the most important updates. We send eNewsletters once a month that cover trending topics and feature updates.