Rendering Text (Part 2): Serialization of Text in NextJS
August 26, 2023
Working Around PortableText to Render Text In the FrontEnd
Next.JS
Why Write A Custom Serializer Component?
Previously, in part 1, one limitation of PortableText was that it couldn't recognize the "code" type. It simply didn't have such type definitions in its library. Unable to display code blocks in my posts, I attempted a work-around where I:
- Created new variable Type Alias that defines properties for two types of incoming block content, either 'code' or 'block'
- Then used a function to conditionally render the normal 'block' text with PortableText and anything 'code' with a new CodeBlock component that rendered the Prism plug-ins flashy code-block text.
This held up until I needed to do some in-line styling of various text to change fonts, size, and other personalized formatting. Any attempt to use the TailwindCSS classNames or global CSS simply wouldn't work!
This was because PortableText was receiving and rendering everything.
The solution required a new, more granular component wrapper that would allow you to control the styling of individual text objects that came from the backend.
Constructing the Serializer Component
- So then I wrote a Serializer component, or simply a wrapper that receives incoming text-block contents as objects, and based on which field it was (<H1> - <H3>, <p>, <span>, and so on).
- One added bonus was that I was able to integrate my CodeBlock component into the Serializer
const serializers = {
types: {
code: (props: BlockContent) => {
return <CodeBlock language={props.language} code={props.code} />;
},
block: (props: BlockContent) => {
const { style, children, markDefs, listItem } = props;
const renderChildren = (children: any[], markDefs: any[]): ReactNode => {
return children.map((child, index) => {
if (child._type === 'span' && child.text) {
if (child.marks && child.marks.length > 0) {
const mark = markDefs.find(def => def._key === child.marks[0]);
if (mark && mark._type === 'link') {
return <a href={mark.href} className="text-blue-500">{child.text}</a>;
}
}
return child.text;
}
return null;
}).filter(Boolean);
};
...//continuing to conditional if statements below
Let's break down the 'serializers' object. The types object has properties: 'code' and 'block'.
When the serializer is passed a 'code' key, it returns the value of a CodeBlock component that we imported from elsewhere (formats into code blocks). This text is referred to as 'block-level content.'
And if we pass into the serializer object a block of text that has the type 'block'?
- We begin by destructuring the props we pass in and get all the goodies that the come along with it: a style property, children (the hierarchy of nested elements), a markDefs library (another object that adds mark information about the text: is it a type link? type bold? type footnote? etc)
- Then simply define the renderChildren function that passes in which maps through that nested heirarchy of html elements
- Mapping: the nested if statement checks those properties we destructured above in the iteration. Is the html element being iterated of type span and has text? If so, we check if it has a 'mark' property because we want to apply anchor (<a/>) JSX to anything which has a mark of type Link. Otherwise we just return the text.
- Finally we add the .filter(Boolean) to filter any null values out.
So now we move on to the list item's that are destructured from props: listItems.
const { style, children, markDefs, listItem } = props;
Within the serializer, using conditional if statements, the blog simply checks if the listItem is a 'bullet', 'number', 'h1 heading', and so on. It then simply applies the corresponding jsx tags and tailwind to the currently rendered text:
const serializer = {
....
const renderedText = renderChildren(children || [], markDefs || []);
// Handle list items with granular control over indentation
if (listItem === 'bullet') {
return <ul className="font-sans text-lg iphone12:text-sm list-disc
mx-8 px-8 py-3"><li>{renderedText}</li></ul>;
}
// Handle heading levels with control over padding and margin and fontsize
if (style === 'h1') {
return <h1 className="font-sans text-5xl iphone12:text-3xl font-
bold mb-3 pb-5 pt-5">{renderedText}</h1>;
}
// Handle normal text with custom font
if (style === 'normal') {
return <p className="font-sans tracking-normal text-xl
iphone12:text-sm whitespace-normal pb-2">{renderedText}</p>;
}
return <p>{renderedText || 'Other types of content.'}</p>;
},
//and so on
},
};
export default serializers;
Then to wrap-up and plug everything in:
In the component where the Post is displayed: The render function maps the post's body text, block by block.
{post.body.map((block, idx) => (
<React.Fragment key={idx}>
{renderBlockContent(block)}
</React.Fragment>
))}
It iterates over the current text block and renderBlockContent passes in the block's 'type' property (block.type) into the serializer to get the final rendered text with our required styles and responsize tailwind CSS.