Last active
September 9, 2025 10:16
-
-
Save elliott-w/6c2e98bdf158a0b1ba4de1e3fd73d225 to your computer and use it in GitHub Desktop.
Payload CMS - Stores richText content as a string rather than a raw object
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
traverseFields, | |
type CollectionConfig, | |
type GlobalConfig, | |
type Plugin, | |
type RichTextField, | |
type TraverseFieldsCallback, | |
} from 'payload' | |
import { richText } from 'payload/shared' | |
// Helper function to detect if an object is a Lexical rich text structure | |
const isLexicalRichText = (value: any): boolean => { | |
return ( | |
value && | |
typeof value === 'object' && | |
value.root && | |
typeof value.root === 'object' && | |
Array.isArray(value.root.children) | |
) | |
} | |
// Helper function to detect if a string is a stringified rich text | |
const isStringifiedRichText = (value: string): boolean => { | |
return value.startsWith('{"root":') | |
} | |
// Helper function to check if rich text data needs processing | |
const needsStringification = (value: any): boolean => { | |
return isLexicalRichText(value) | |
} | |
const needsParsing = (value: any): boolean => { | |
return typeof value === 'string' && isStringifiedRichText(value) | |
} | |
// Create field-level hooks for richText fields | |
const addRichTextHooks = (field: RichTextField): void => { | |
const originalValidate = field.validate | |
field.validate = async (value: any, options: any) => { | |
const incomingValue = value as unknown as string | undefined | |
const parsedValue = incomingValue ? JSON.parse(incomingValue) : undefined | |
const result = await originalValidate?.(parsedValue, options) | |
if (typeof result === 'string') { | |
return result | |
} | |
return richText(parsedValue, { | |
...options, | |
name: field.name, | |
type: 'richText', | |
}) | |
} | |
if (!field.hooks) { | |
field.hooks = {} | |
} | |
field.hooks.beforeChange = [ | |
...(field.hooks.beforeChange || []), | |
async ({ value, req }) => { | |
try { | |
// Only stringify if it's currently a rich text object | |
if (needsStringification(value)) { | |
return JSON.stringify(value) | |
} | |
// If it's already a string or other format, leave it alone (backwards compatible) | |
return value | |
} catch (error) { | |
req.payload.logger.error( | |
`RichTextStringify: Error in field beforeChange for ${field.name}:`, | |
error | |
) | |
return value | |
} | |
}, | |
] | |
field.hooks.afterRead = [ | |
...(field.hooks.afterRead || []), | |
async ({ value, req }) => { | |
try { | |
// Parse stringified rich text back to objects | |
if (needsParsing(value)) { | |
return JSON.parse(value) | |
} | |
// If it's already an object (legacy format), leave it alone (backwards compatible) | |
return value | |
} catch (error) { | |
req.payload.logger.error( | |
`RichTextStringify: Error in field afterRead for ${field.name}:`, | |
error | |
) | |
// Keep original value if parsing fails | |
return value | |
} | |
}, | |
] | |
} | |
interface RichTextStringifyPluginArgs { | |
/** | |
* Whether to enable the plugin. | |
* @default true | |
*/ | |
enable?: boolean | |
} | |
// Main plugin function | |
export const richTextStringifyPlugin = ({ | |
enable = true, | |
}: RichTextStringifyPluginArgs = {}): Plugin => { | |
return (config) => { | |
if (!enable) { | |
return config | |
} | |
const addRichTextHooksCallback: TraverseFieldsCallback = ({ field }) => { | |
if (field.type === 'richText') { | |
addRichTextHooks(field as RichTextField) | |
} | |
} | |
// Process collections to add field-level hooks | |
;(config.collections || []).forEach((collection: CollectionConfig) => { | |
traverseFields({ | |
fields: collection.fields, | |
callback: addRichTextHooksCallback, | |
}) | |
}) | |
// Process globals to add field-level hooks | |
config.globals = (config.globals || []).map((global: GlobalConfig) => { | |
traverseFields({ | |
fields: global.fields, | |
callback: addRichTextHooksCallback, | |
}) | |
return global | |
}) | |
return config | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment