From 36a6811dcfb08f4975edf22aa06aa75a83e2e9d3 Mon Sep 17 00:00:00 2001 From: Chris Arter Date: Sat, 9 May 2026 09:32:44 -0400 Subject: [PATCH] fix(lexical): add codeBlock + complete TextNode shape Payload's defaultFeatures don't register a 'code' node, so fenced code blocks crashed the admin editor with $parseSerializedNodeImpl. Replace the bare 'code' Lexical node with a custom Payload Block ('codeBlock') exposing language + code fields, register it in the body editor, and render it as
 on the frontend.

Also fill in the TextNode serialized shape (detail/mode/style) that
Lexical 0.41's TextNode.updateFromJSON requires.

Co-Authored-By: Claude Opus 4.7 (1M context) 
---
 scripts/import/markdown-to-lexical.ts | 45 ++++++++++-------------
 src/app/(payload)/admin/importMap.js  | 53 +++++++++++++++++++++++++--
 src/blocks/codeBlock.ts               | 10 +++++
 src/blocks/index.ts                   |  1 +
 src/components/lexical/Render.tsx     |  8 ++++
 src/lib/lexical-editor.ts             |  4 +-
 src/payload-types.ts                  | 11 ++++++
 7 files changed, 102 insertions(+), 30 deletions(-)
 create mode 100644 src/blocks/codeBlock.ts

diff --git a/scripts/import/markdown-to-lexical.ts b/scripts/import/markdown-to-lexical.ts
index 4c34198..8febf74 100644
--- a/scripts/import/markdown-to-lexical.ts
+++ b/scripts/import/markdown-to-lexical.ts
@@ -12,8 +12,16 @@ type LexNode = Record
 
 const HTML_IMG = /]+src=["'](\/images\/[^"']+)["'][^>]*?(?:alt=["']([^"']*)["'])?[^>]*?\/?>/g
 
-function textNode(value: string): LexNode {
-  return { type: 'text', text: value, format: 0, version: 1 }
+function textNode(value: string, format = 0): LexNode {
+  return {
+    type: 'text',
+    text: value,
+    format,
+    detail: 0,
+    mode: 'normal',
+    style: '',
+    version: 1,
+  }
 }
 
 function paragraph(children: LexNode[]): LexNode {
@@ -118,21 +126,16 @@ async function convertMdast(node: Content, ctx: Ctx): Promise {
 async function convertInline(node: Content, ctx: Ctx): Promise {
   if (node.type === 'text') return textNode(node.value)
   if (node.type === 'strong')
-    return {
-      type: 'text',
-      text: (node.children as { value?: string }[]).map((c) => c.value ?? '').join(''),
-      format: 1,
-      version: 1,
-    }
+    return textNode(
+      (node.children as { value?: string }[]).map((c) => c.value ?? '').join(''),
+      1,
+    )
   if (node.type === 'emphasis')
-    return {
-      type: 'text',
-      text: (node.children as { value?: string }[]).map((c) => c.value ?? '').join(''),
-      format: 2,
-      version: 1,
-    }
-  if (node.type === 'inlineCode')
-    return { type: 'text', text: node.value, format: 16, version: 1 }
+    return textNode(
+      (node.children as { value?: string }[]).map((c) => c.value ?? '').join(''),
+      2,
+    )
+  if (node.type === 'inlineCode') return textNode(node.value, 16)
   if (node.type === 'link') {
     return {
       type: 'link',
@@ -178,15 +181,7 @@ async function convertCode(node: Code, ctx: Ctx): Promise {
       content: innerLex,
     })
   }
-  return {
-    type: 'code',
-    language: node.lang ?? '',
-    children: [textNode(node.value)],
-    direction: 'ltr',
-    format: '',
-    indent: 0,
-    version: 1,
-  }
+  return block('codeBlock', { language: node.lang ?? '', code: node.value })
 }
 
 function emptyRoot(): LexNode {
diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js
index 7a40249..45ffff2 100644
--- a/src/app/(payload)/admin/importMap.js
+++ b/src/app/(payload)/admin/importMap.js
@@ -1,9 +1,56 @@
+import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
+import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
+import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
+import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
 import { R2ClientUploadHandler as R2ClientUploadHandler_85cc02ed84006fcc91d3aff39dda669d } from '@payloadcms/storage-r2/client'
 import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
 
 /** @type import('payload').ImportMap */
 export const importMap = {
-  '@payloadcms/storage-r2/client#R2ClientUploadHandler':
-    R2ClientUploadHandler_85cc02ed84006fcc91d3aff39dda669d,
-  '@payloadcms/next/rsc#CollectionCards': CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
+  "@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
+  "@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
+  "@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
+  "@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+  "@payloadcms/storage-r2/client#R2ClientUploadHandler": R2ClientUploadHandler_85cc02ed84006fcc91d3aff39dda669d,
+  "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
 }
diff --git a/src/blocks/codeBlock.ts b/src/blocks/codeBlock.ts
new file mode 100644
index 0000000..fc56100
--- /dev/null
+++ b/src/blocks/codeBlock.ts
@@ -0,0 +1,10 @@
+import type { Block } from 'payload'
+
+export const CodeBlock: Block = {
+  slug: 'codeBlock',
+  interfaceName: 'CodeBlock',
+  fields: [
+    { name: 'language', type: 'text' },
+    { name: 'code', type: 'textarea', required: true },
+  ],
+}
diff --git a/src/blocks/index.ts b/src/blocks/index.ts
index fd4120b..b44c4d9 100644
--- a/src/blocks/index.ts
+++ b/src/blocks/index.ts
@@ -1,3 +1,4 @@
 export { LinkPreviewBlock } from './linkPreview'
 export { CalloutBlock } from './callout'
 export { MediaBlock } from './mediaBlock'
+export { CodeBlock } from './codeBlock'
diff --git a/src/components/lexical/Render.tsx b/src/components/lexical/Render.tsx
index 019a18c..1e22e9d 100644
--- a/src/components/lexical/Render.tsx
+++ b/src/components/lexical/Render.tsx
@@ -48,6 +48,7 @@ function renderNode(node: Node): ReactNode {
           
         
) + // codeBlock handled in 'block' case below case 'horizontalrule': return
case 'link': { @@ -89,6 +90,13 @@ function renderNode(node: Node): ReactNode { const alt = (typeof f.media === 'object' && f.media?.alt) || f.caption || '' return } + if (f.blockType === 'codeBlock') { + return ( +
+            {f.code}
+          
+ ) + } return null } default: diff --git a/src/lib/lexical-editor.ts b/src/lib/lexical-editor.ts index 778b24a..349a2d4 100644 --- a/src/lib/lexical-editor.ts +++ b/src/lib/lexical-editor.ts @@ -4,13 +4,13 @@ import { HorizontalRuleFeature, lexicalEditor, } from '@payloadcms/richtext-lexical' -import { LinkPreviewBlock, CalloutBlock, MediaBlock } from '@/blocks' +import { LinkPreviewBlock, CalloutBlock, MediaBlock, CodeBlock } from '@/blocks' export const bodyEditor = lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4', 'h5', 'h6'] }), HorizontalRuleFeature(), - BlocksFeature({ blocks: [LinkPreviewBlock, CalloutBlock, MediaBlock] }), + BlocksFeature({ blocks: [LinkPreviewBlock, CalloutBlock, MediaBlock, CodeBlock] }), ], }) diff --git a/src/payload-types.ts b/src/payload-types.ts index 1113746..4d1e220 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -607,6 +607,17 @@ export interface MediaBlock { blockName?: string | null; blockType: 'mediaBlock'; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "CodeBlock". + */ +export interface CodeBlock { + language?: string | null; + code: string; + id?: string | null; + blockName?: string | null; + blockType: 'codeBlock'; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "auth". -- 2.51.2