diff --git a/docs/rich-text/converting-jsx.mdx b/docs/rich-text/converting-jsx.mdx
index cf924d21a05..c72fccfb791 100644
--- a/docs/rich-text/converting-jsx.mdx
+++ b/docs/rich-text/converting-jsx.mdx
@@ -81,6 +81,60 @@ export const MyComponent: React.FC<{
}
```
+### Custom Components
+
+Both `LinkJSXConverter` and `UploadJSXConverter` accept optional component props, so you can use framework-specific components (e.g. `next/link`, `next/image`) without overriding the full converter logic:
+
+```tsx
+import {
+ type JSXConvertersFunction,
+ LinkJSXConverter,
+ UploadJSXConverter,
+} from '@payloadcms/richtext-lexical/react'
+import Image from 'next/image'
+import Link from 'next/link'
+
+const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
+ ...defaultConverters,
+ ...LinkJSXConverter({
+ internalDocToHref: ({ linkNode }) => `/${linkNode.fields.doc?.value?.slug}`,
+ LinkComponent: ({ href, children, ...rest }) => (
+
+ {children}
+
+ ),
+ }),
+ ...UploadJSXConverter({
+ buildFullUrl: (path) => `https://cdn.example.com${path}`,
+ ImageComponent: ({ alt, height, src, width }) => (
+
+ ),
+ LinkComponent: ({ href, children, ...rest }) => (
+
+ {children}
+
+ ),
+ }),
+})
+```
+
+**`LinkJSXConverter` options:**
+
+- **`LinkComponent`** – Replaces `` for both autolink and link nodes. Receives `href`, `rel`, `target`, and `children`.
+
+**`UploadJSXConverter` options:**
+
+- **`ImageComponent`** – Replaces `
` for images. For images with sizes, it replaces the fallback `
` inside ``.
+- **`LinkComponent`** – Replaces `` for non-image file uploads.
+- **`buildFullUrl`** – Transforms upload paths into absolute URLs (e.g. for CDN). Applied to the main URL and all size URLs.
+
+All parameters are optional — calling `UploadJSXConverter()` with no arguments or `LinkJSXConverter({})` produces the same defaults as before.
+
### Lexical Blocks
If your rich text includes custom Blocks or Inline Blocks, you must supply custom converters that match each block's slug. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
@@ -137,7 +191,9 @@ export const MyComponent: React.FC<{
You can override any of the default JSX converters by passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.
-Example - overriding the upload node converter to use next/image:
+For common customizations like using `next/image` or `next/link`, use the built-in component props on `UploadJSXConverter` and `LinkJSXConverter` as shown in the [Custom Components](#custom-components) section above.
+
+For fully custom rendering logic, you can override a converter entirely by providing your own function keyed to the node type:
```tsx
'use client'
diff --git a/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/link.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/link.tsx
index 6c8fe655303..bba67c17ca7 100644
--- a/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/link.tsx
+++ b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/link.tsx
@@ -3,7 +3,16 @@ import type { JSXConverters } from '../types.js'
export const LinkJSXConverter: (args: {
internalDocToHref?: (args: { linkNode: SerializedLinkNode }) => string
-}) => JSXConverters = ({ internalDocToHref }) => ({
+ LinkComponent?: React.ComponentType<{
+ children: React.ReactNode
+ href: string
+ rel?: string
+ target?: string
+ }>
+}) => JSXConverters = ({
+ internalDocToHref,
+ LinkComponent,
+}) => ({
autolink: ({ node, nodesToJSX }) => {
const children = nodesToJSX({
nodes: node.children,
@@ -12,6 +21,14 @@ export const LinkJSXConverter: (args: {
const rel: string | undefined = node.fields.newTab ? 'noopener noreferrer' : undefined
const target: string | undefined = node.fields.newTab ? '_blank' : undefined
+ if (LinkComponent) {
+ return (
+
+ {children}
+
+ )
+ }
+
return (
{children}
@@ -38,6 +55,14 @@ export const LinkJSXConverter: (args: {
}
}
+ if (LinkComponent) {
+ return (
+
+ {children}
+
+ )
+ }
+
return (
{children}
diff --git a/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/upload.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/upload.tsx
index 927950d1dc8..f5a39381bf0 100644
--- a/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/upload.tsx
+++ b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/upload.tsx
@@ -4,7 +4,25 @@ import type { SerializedUploadNode } from '../../../../../nodeTypes.js'
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.js'
import type { JSXConverters } from '../types.js'
-export const UploadJSXConverter: JSXConverters = {
+export const UploadJSXConverter: (args?: {
+ buildFullUrl?: (path: string) => string
+ ImageComponent?: React.ComponentType<{
+ alt?: string
+ height?: number
+ src: string
+ width?: number
+ }>
+ LinkComponent?: React.ComponentType<{
+ children: React.ReactNode
+ href: string
+ rel?: string
+ target?: string
+ }>
+}) => JSXConverters = ({
+ buildFullUrl,
+ ImageComponent,
+ LinkComponent,
+} = {}) => ({
upload: ({ node }) => {
// TO-DO (v4): SerializedUploadNode should use UploadData_P4
const uploadNode = node as UploadDataImproved
@@ -16,12 +34,20 @@ export const UploadJSXConverter: JSXConverters = {
const alt = (uploadNode.fields?.alt as string) || (uploadDoc as { alt?: string })?.alt || ''
- const url = uploadDoc.url
+ const url = buildFullUrl && uploadDoc.url ? buildFullUrl(uploadDoc.url) : uploadDoc.url
/**
* If the upload is not an image, return a link to the upload
*/
if (!uploadDoc.mimeType.startsWith('image')) {
+ if (LinkComponent) {
+ return (
+
+ {uploadDoc.filename}
+
+ )
+ }
+
return (
{uploadDoc.filename}
@@ -33,6 +59,17 @@ export const UploadJSXConverter: JSXConverters = {
* If the upload is a simple image with no different sizes, return a simple img tag
*/
if (!uploadDoc.sizes || !Object.keys(uploadDoc.sizes).length) {
+ if (ImageComponent) {
+ return (
+
+ )
+ }
+
return
}
@@ -57,7 +94,9 @@ export const UploadJSXConverter: JSXConverters = {
) {
continue
}
- const imageSizeURL = imageSize?.url
+
+ const imageSizeURL =
+ buildFullUrl && imageSize.url ? buildFullUrl(imageSize.url) : imageSize.url
pictureJSX.push(
= {
}
// Add the default img tag
- pictureJSX.push(
-
,
- )
+ if (ImageComponent) {
+ pictureJSX.push(
+ ,
+ )
+ } else {
+ pictureJSX.push(
+
,
+ )
+ }
+
return {pictureJSX}
},
-}
+})
diff --git a/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/defaultConverters.ts b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/defaultConverters.ts
index f00439be97c..d6cdb78bd78 100644
--- a/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/defaultConverters.ts
+++ b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/defaultConverters.ts
@@ -23,6 +23,6 @@ export const defaultJSXConverters: JSXConverters = {
...HorizontalRuleJSXConverter,
...ListJSXConverter,
...LinkJSXConverter({}),
- ...UploadJSXConverter,
+ ...UploadJSXConverter(),
...TabJSXConverter,
}