import type { JSXConvertersFunction } from '@payloadcms/richtext-lexical/react' import type { SerializedLinkNode } from '@payloadcms/richtext-lexical' import { Button } from '@/components/Button/Button' // Lexical link nodes carry their editable metadata (url, newTab, linkType, ...) // in `fields`. We extend that shape with the custom `appearance` select that is // added to LinkFeature in `payload.config.ts`, so editors can mark a link as a // call-to-action button. type LinkFields = SerializedLinkNode['fields'] & { appearance?: 'link' | 'button' } // Custom JSX converters passed to wherever rich text is rendered. // Only the `link` converter is overridden — every other node keeps Payload's // default rendering via the spread of `defaultConverters`. export const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({ ...defaultConverters, link: (args) => { const { node, nodesToJSX } = args const fields = node.fields as LinkFields // Normal link → delegate to Payload's built-in link converter. // The default converter from the package is a function, but the type // also allows a static ReactNode, so we narrow before calling. if (fields?.appearance !== 'button') { const defaultLink = defaultConverters.link return typeof defaultLink === 'function' ? defaultLink(args) : defaultLink } // Button link → resolve href the same way Payload's default converter does: // internal links point at the related doc's slug, custom links use `url`. const href = fields.linkType === 'internal' && typeof fields.doc?.value === 'object' ? `/${(fields.doc.value as { slug?: string }).slug ?? ''}` : (fields.url ?? '#') // Render the existing Button component. Schema and size are intentionally // hardcoded — editors only choose link vs. button, not the styling. return ( ) }, })