Dealing with annoying JSX conversion
Ok it looks like I'm slowly getting a hang of this Payload CMS (I hope). I made some custom building blocks, now I wanna deal with looks and styling. The majority of the content is handled through the CMS admin, and now I need to make it match the design. Namely, text properties like color.
I have two custom "brand" colors, and I need to set up a way to apply a color to text in the rich text editor. Payload uses the Lexical editor, and by default it only has a few of the most basic stylings like headings, bold, italic, etc. (Eventually I activated more of them and moved them to defaultLexical.ts because why not?)
First I thought - hey, I'm using Tailwind, wouldn't it be cool if I could select a text, and then type in some Tailwind classes to be applied to that text? That way I can apply not only colors, but pretty much anything I want! Sadly, that wouldn't work - Tailwind only picks up class names it can see at build time. So that idea is out the window.
The way Lexical deals with this is with something called TextStateFeature. You can define text state properties like color, background, underline and such, to be used in the editor. Luckily, it already has a set of defaultColors that are suitable for usage in the rich text editor, so that we don't have to define all the properties (like labels and IDs) needed for it ourselves (I think that would just be silly). I imported them all and added my custom brand colors in a small map:
1// customColorMap.ts2export const customColorMap = {3 brand: { label: 'Brand', css: { color: '#226eff' } },4 brandLight: { label: 'Brand Light', css: { color: '#2297ff' } },5}
1// defaultLexical.ts (excerpt)2TextStateFeature({3 state: {4 color: {5 ...defaultColors.text,6 ...defaultColors.background,7 ...customColorMap,8 },9 },10})
That part felt reasonable. Pick a color in admin, see it in the editor. Done, right?
Now comes the annoying part - making it render in the actual content on the frontend. For that, we have something called JSX converters. I dealt with a very basic example of that earlier, but this seemed like a step up in complexity.
Eventually I figured out that I need to add a new text property to the JSX converter object and define my custom rules there. Basically, for each text node, I need to access its properties for styling and map them to actual CSS values. In case of colors, I need to access node['$']?.color to find the color key (not value) - and then find the color value in my color maps using that color key. (BTW I find that "$" naming kinda weird and unintuitive.) For example, brand or brandLight maps to my custom color. Then I put that color value into style.color and apply that style to the text node:
1text: ({ node }) => {2 const style: React.CSSProperties = {}3 const colorKey = node['$']?.color45 if (typeof colorKey === 'string') {6 const colorValue = customColorMap[colorKey]?.css?.color7 if (colorValue) {8 style.color = colorValue9 }10 }1112 return <span style={style}>{node.text}</span>13}
Brand colors showed up on the site. Small win.
Then I tried to do the sensible thing and reuse the same color definitions as in the editor - merge defaultColors with customColorMap in the converter too, so I would not maintain two lists. But for some reason it would not let me import defaultColors from @payloadcms/richtext-lexical in RichText/index.tsx the way it did in my editor config and customColorMap util - something to do with imports... I don't believe the idea is to define every single color myself here, even the existing default ones from the library - that would be just silly. There's gotta be a way to do this. I don't have time for this right now, I'll come back to it later. For now I'll just use my two custom brand colors since using those is fine.
OH, and if you also thought node['$'].color is weird or unintuitive, guess how things like bold or italic are marked?
There is a node.format property, and you'd think the value would be something like "bold" or "italic" or anything vaguely related... But no... it's just numbers... like 1 is bold, 2 is italic... <code> is 16... And I'm supposed to just go through my text and manually figure out what is what? Whatever.
1if (node.format === 1) return <strong className="text-foreground">{node.text}</strong>2if (node.format === 2) return <em>{node.text}</em>3if (node.format === 16)4 return (5 <code className="bg-gray-800 p-1 rounded text-brand-primary font-normal">{node.text}</code>6 )78return <span style={style}>{node.text}</span>
So the editor side and the frontend side are two different jobs. TextStateFeature gives authors a nice picker; JSX converters make you a translator between whatever Lexical serialized and actual React/CSS. I'm not sure I'm done with this file - default palette colors from admin still might not render until I solve that import thing - but at least my brand colors and basic formatting appear on the page.
Related Posts:
My first time dealing with RichText with a headless CMS and NextJS
Notes from my first time wiring Contentful Rich Text into Next.js 13 - customizing links with documentToReactComponents options