My first time dealing with RichText with a headless CMS and NextJS
Originally written on
Rich Text from a CMS is new to me. Even though I assumed I'll have to massage the output somewhat (styles and formatting being applied exactly the way I want sounds too good to be true), I'm beginning to get the idea of how much work falls on you when you're turning the document into real markup. This write-up is only about one small slice of that: getting links to work how I want on a Next.js page that pulls from Contentful.
I'm (still) on Next.js 13 with the pages router, Contentful CMS v9, and @contentful/rich-text-react-renderer so I'm not hand-walking the JSON. The first time I hooked up documentToReactComponents, it almost felt too easy: grab the entry, pass the rich text field in, and there are your paragraphs and emphasis. Then the rough edges start appearing.
The first one, though, wasn't a CMS thing, but a TypeScript thing. Whatever types I have, the value doesn't always satisfy what the renderer wants as a Document, so I'm using a cast for now as a workaround - mainContent as Document inside the call. Not my favorite pattern, but I'm choosing my battles...
The link part feels more properly "rich text shaped." The user is adding links in the CMS; I get real hyperlink nodes in the tree. The default output works, but because most pages have external links, I want them opening in new tabs and rel="noopener noreferrer" on those anchors, and that's not something I can toggle in the dashboard. So I need to reach into the renderer and modify those links.
That's what the second argument to documentToReactComponents is for: an options object with a renderNode map. For inline links, @contentful/rich-text-types gives you INLINES.HYPERLINK, and the URL lives on node.data.uri. I wire that up once and leave the rest of the document on the library defaults until the next time.
1import { documentToReactComponents } from '@contentful/rich-text-react-renderer';2import { Document, INLINES, Node } from '@contentful/rich-text-types';3import { ReactNode } from 'react';45const renderOptions = {6 renderNode: {7 [INLINES.HYPERLINK]: (node: Node, children: ReactNode) => {8 return (9 <a href={node.data.uri} rel="noopener noreferrer" target="_blank">10 {children}11 </a>12 );13 },14 },15};1617// In JSX (e.g. your page component):18{documentToReactComponents(mainContent as Document, renderOptions)}19
This still feels like one of the simpler cases - same <a>, different attributes. Is it always going to be like this? I probably shouldn't get my hopes up too high.
The Next.js Hydration error and Dynamic imports
If you’ve worked with Next.js, you’ve probably seen this error before. It’s the one where the browser complains that "Text content does not match server-rendered HTML"...