vault backup: 2026-05-15 16:43:24
This commit is contained in:
parent
60fad12c7a
commit
7f83c117d9
6 changed files with 351 additions and 1 deletions
|
|
@ -35,7 +35,7 @@ This 3-hop pattern works for hundreds of articles without vector search.
|
|||
| [[wiki/reports/_index\|reports/]] | Weekly and monthly summaries — generate: `uv run python scripts/report-generator.py --weekly` | 1 |
|
||||
| [[wiki/infrastructure/_index\|infrastructure/]] | Server inventory: all 10 SSH hosts — optical, optical-dev, optical-prod, baic, librechat, modocmms, box-cli, aimpress, pve | 12 |
|
||||
| [[wiki/testing/_index\|testing/]] | Web app testing: functional, performance, security, UI types; TDD/BDD/Agile methodologies; Selenium/Cypress/Playwright/JMeter/OWASP ZAP tools | 1 |
|
||||
| [[wiki/payloadcms/_index\|payloadcms/]] | Full Payload CMS reference — getting started, config, database (Postgres/MongoDB/SQLite), all 22 field types, access control, hooks, authentication (cookies, JWT, API keys, custom strategies, token data), admin UI, custom components, Lexical rich text, live preview, versions/drafts, Local/REST/GraphQL APIs, queries, plugins, jobs queue, upload, ecommerce, production deploy, TypeScript, migration guides, i18n, localization, hierarchy | 135 |
|
||||
| [[wiki/payloadcms/_index\|payloadcms/]] | Full Payload CMS reference — getting started, config, database (Postgres/MongoDB/SQLite), all 22 field types, access control, hooks, authentication (cookies, JWT, API keys, custom strategies, token data), admin UI, custom components, Lexical rich text, live preview, versions/drafts, Local/REST/GraphQL APIs, queries, plugins, jobs queue, upload, ecommerce, production deploy, TypeScript, migration guides, i18n, localization, hierarchy | 136 |
|
||||
|
||||
| [[wiki/shared-patterns/_index\|shared-patterns/]] | Oliver Agency standard library patterns: httpx, structlog, pydantic-settings, alembic — reuse before writing from scratch | 4 |
|
||||
| [[wiki/mistakes/_index\|mistakes/]] | Anti-patterns extracted from sessions — per-stack running lists (fastapi, react, docker, postgres, general) — injected at session start | 5 |
|
||||
|
|
|
|||
|
|
@ -136,3 +136,4 @@
|
|||
| [[wiki/payloadcms/rich-text-converting-html\|Rich Text — Converting HTML]] | `convertLexicalToHTML` (sync/async), `convertHTMLToLexical`, image data-attributes pattern, `buildEditorState` helper, block converters | raw/rich-text__converting-html.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/rich-text-converting-jsx\|Rich Text — Converting to JSX]] | `RichText` component, custom converters function, internal link `internalDocToHref`, custom block converters per slug, overriding built-ins (e.g. upload → next/image) | raw/rich-text__converting-jsx.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/rich-text-converting-markdown\|Rich Text — Converting Markdown]] | `convertLexicalToMarkdown` / `convertMarkdownToLexical`, editorConfigFactory, sibling textarea hook pattern, Upload `![media:<id>]()` placeholder, MDX block `jsx.export`/`jsx.import` | raw/rich-text__converting-markdown.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/rich-text-converting-plaintext\|Rich Text — Converting to Plaintext]] | `convertLexicalToPlaintext`, no built-in defaults, fallback heuristics (text→children→ignore), paragraph/text/tab newlines, custom `PlaintextConverters` | raw/rich-text__converting-plaintext.md | 2026-05-15 |
|
||||
|
|
|
|||
84
wiki/payloadcms/rich-text-converting-plaintext.md
Normal file
84
wiki/payloadcms/rich-text-converting-plaintext.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
title: "Rich Text — Converting to Plaintext"
|
||||
aliases: [lexical-to-plaintext, convert-richtext-plaintext]
|
||||
tags: [payloadcms, lexical, rich-text, plaintext, converters]
|
||||
sources: [raw/rich-text__converting-plaintext.md]
|
||||
created: 2026-05-15
|
||||
updated: 2026-05-15
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`@payloadcms/richtext-lexical/plaintext` provides `convertLexicalToPlaintext` to strip Lexical JSON down to a plain string — useful for search indexing, excerpts, or email previews.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```ts
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
import { convertLexicalToPlaintext } from '@payloadcms/richtext-lexical/plaintext'
|
||||
|
||||
const data: SerializedEditorState = { /* your richtext data */ }
|
||||
|
||||
const plaintext = convertLexicalToPlaintext({ data })
|
||||
```
|
||||
|
||||
## Custom Converters
|
||||
|
||||
Pass a `converters` object to override how specific node types are rendered.
|
||||
|
||||
```ts
|
||||
import type { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
import type { MyTextBlock } from '@/payload-types'
|
||||
import {
|
||||
convertLexicalToPlaintext,
|
||||
type PlaintextConverters,
|
||||
} from '@payloadcms/richtext-lexical/plaintext'
|
||||
|
||||
const converters: PlaintextConverters<
|
||||
DefaultNodeTypes | SerializedBlockNode<MyTextBlock>
|
||||
> = {
|
||||
blocks: {
|
||||
textBlock: ({ node }) => node.fields.text ?? '',
|
||||
},
|
||||
link: ({ node }) => node.fields.url ?? '',
|
||||
}
|
||||
|
||||
const plaintext = convertLexicalToPlaintext({ converters, data })
|
||||
```
|
||||
|
||||
## Fallback Heuristics
|
||||
|
||||
Unlike HTML/JSX/Markdown converters, **there are no built-in default converters** for plaintext. For nodes without a defined converter, the following heuristics apply in order:
|
||||
|
||||
| Node has… | Behavior |
|
||||
|-----------|----------|
|
||||
| `text` field | Use it as plaintext |
|
||||
| `children` field | Recursively convert children |
|
||||
| Neither | Ignored (node produces no output) |
|
||||
|
||||
Special nodes:
|
||||
- **Paragraph** → inserts a newline (`\n`)
|
||||
- **Text** → inserts the text value
|
||||
- **Tab** → inserts a tab character (`\t`)
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- Import from `@payloadcms/richtext-lexical/plaintext` (separate entrypoint).
|
||||
- No default converters — you must handle custom block/node types yourself or rely on the fallback heuristics.
|
||||
- Fallback order: `text` field → `children` (recursive) → ignored.
|
||||
- Paragraph/text/tab nodes emit `\n` / text / `\t` automatically.
|
||||
- Use `PlaintextConverters<DefaultNodeTypes | SerializedBlockNode<YourBlock>>` for TypeScript-safe custom converters.
|
||||
- Common use cases: full-text search indexing, email body generation, excerpt/preview generation.
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/payloadcms/rich-text-converters|Rich Text — Converters Overview]] — HTML, JSX, Markdown, Plaintext converter architecture
|
||||
- [[wiki/payloadcms/rich-text-converting-html|Rich Text — Converting HTML]] — `convertLexicalToHTML` and `convertHTMLToLexical`
|
||||
- [[wiki/payloadcms/rich-text-converting-jsx|Rich Text — Converting to JSX]] — `RichText` component and custom JSX converters
|
||||
- [[wiki/payloadcms/rich-text-converting-markdown|Rich Text — Converting Markdown]] — Markdown ↔ Lexical round-trip
|
||||
|
||||
## Sources
|
||||
|
||||
- `raw/rich-text__converting-plaintext.md`
|
||||
- https://payloadcms.com/docs/rich-text/converting-plaintext
|
||||
265
wiki/payloadcms/rich-text-custom-features.md
Normal file
265
wiki/payloadcms/rich-text-custom-features.md
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
---
|
||||
title: "Rich Text — Custom Lexical Features"
|
||||
aliases: [lexical-custom-features, custom-lexical-feature, payload-lexical-feature]
|
||||
tags: [payloadcms, lexical, rich-text, custom-feature, nodes, toolbar, slash-menu]
|
||||
sources: [raw/rich-text__custom-features.md]
|
||||
created: 2026-05-15
|
||||
updated: 2026-05-15
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Custom Lexical features are modular extensions to the Payload rich text editor. Each feature is split into exactly two files:
|
||||
|
||||
- `feature.server.ts` — server-side entry point (nodes, markdown transformers, i18n, hooks)
|
||||
- `feature.client.ts` — client-side UI (toolbar, slash menu, plugins, React node components)
|
||||
|
||||
The server feature is the sole entry point; it registers the client feature via an import path.
|
||||
|
||||
> **IMPORTANT:** Never import directly from `lexical`, `@lexical/*`, or `@payloadcms/richtext-lexical` on the client side. Use the re-exported paths instead:
|
||||
> - Server/shared: `@payloadcms/richtext-lexical/lexical/utils`
|
||||
> - Client: `@payloadcms/richtext-lexical/client`
|
||||
|
||||
## Do You Need a Custom Feature?
|
||||
|
||||
Before building a custom feature, consider [[wiki/payloadcms/rich-text-blocks|BlocksFeature]] — it supports:
|
||||
- **Block blocks** — full-line custom blocks with arbitrary React components
|
||||
- **Inline blocks** — insertable within paragraphs
|
||||
|
||||
Only build a custom feature if BlocksFeature cannot fulfill the requirement (e.g. custom nodes with lexical commands, toolbar groups, complex markdown transforms).
|
||||
|
||||
## Server Feature
|
||||
|
||||
### Minimal scaffold
|
||||
|
||||
```ts
|
||||
import { createServerFeature } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const MyFeature = createServerFeature({
|
||||
feature: {},
|
||||
key: 'myFeature',
|
||||
})
|
||||
```
|
||||
|
||||
Register in editor config:
|
||||
|
||||
```ts
|
||||
editor: lexicalEditor({ features: [MyFeature()] })
|
||||
```
|
||||
|
||||
### Server Feature properties
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `i18n` | Translations scoped to the feature key (`lexical:<key>:<label>`) |
|
||||
| `markdownTransformers` | `ElementTransformer`/`TextMatchTransformer` for markdown ↔ editor conversion |
|
||||
| `nodes` | Nodes wrapped with `createNode()` — controls HTML converters, hooks, sub-fields |
|
||||
| `ClientFeature` | Import path to the client feature file (`'./path/to/feature.client#ExportName'`) |
|
||||
| `clientFeatureProps` | Serializable props passed to the client feature (must be JSON-serializable) |
|
||||
|
||||
### Node options (via `createNode`)
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `node` | The Lexical node class (supports node replacements) |
|
||||
| `converters.html` | How the node serializes to HTML |
|
||||
| `getSubFields` / `getSubFieldsData` | Sub-field schema + data for auto-population and hooks (REST only) |
|
||||
| `graphQLPopulationPromises` | Population logic for GraphQL (REST hooks don't run in GraphQL context) |
|
||||
| `validations` | Custom node-level validation |
|
||||
| `hooks` | Node Hooks (same lifecycle as field hooks) |
|
||||
|
||||
### Feature load order
|
||||
|
||||
```ts
|
||||
createServerFeature({
|
||||
feature: ({ featureProviderMap, props, config, resolvedFeatures }) => {
|
||||
// featureProviderMap contains ALL features even yet-to-be-loaded
|
||||
return { /* ... */ }
|
||||
},
|
||||
key: 'myFeature',
|
||||
dependenciesPriority: ['otherFeature'], // must load before this one
|
||||
dependencies: ['requiredFeature'], // must exist, any order
|
||||
dependenciesSoft: ['optionalFeature'], // best-effort ordering
|
||||
})
|
||||
```
|
||||
|
||||
## Client Feature
|
||||
|
||||
### Minimal scaffold
|
||||
|
||||
```ts
|
||||
'use client'
|
||||
import { createClientFeature } from '@payloadcms/richtext-lexical/client'
|
||||
|
||||
export const MyClientFeature = createClientFeature({})
|
||||
```
|
||||
|
||||
Register from server feature:
|
||||
|
||||
```ts
|
||||
feature: {
|
||||
ClientFeature: './path/to/feature.client#MyClientFeature',
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Nodes (client side)
|
||||
|
||||
Add node to the `nodes` array in **both** client and server features.
|
||||
|
||||
Example `DecoratorNode`:
|
||||
|
||||
```ts
|
||||
import { DecoratorNode, $applyNodeReplacement } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
export class MyNode extends DecoratorNode<React.ReactElement> {
|
||||
static getType() { return 'myNode' }
|
||||
static clone(node: MyNode) { return new MyNode(node.__key) }
|
||||
static importJSON(serialized): MyNode { return $createMyNode() }
|
||||
|
||||
createDOM(): HTMLElement { return document.createElement('div') }
|
||||
decorate(): React.ReactElement { return <MyNodeComponent nodeKey={this.__key} /> }
|
||||
exportJSON() { return { type: 'myNode', version: 1 } }
|
||||
isInline(): false { return false }
|
||||
updateDOM(): boolean { return false }
|
||||
}
|
||||
|
||||
export function $createMyNode(): MyNode {
|
||||
return $applyNodeReplacement(new MyNode())
|
||||
}
|
||||
export function $isMyNode(node): node is MyNode { return node instanceof MyNode }
|
||||
```
|
||||
|
||||
- Do **not** add `'use client'` to node files — they are used on the server too
|
||||
- Lazy-import React components inside the node via `React.lazy()`
|
||||
|
||||
### Plugins
|
||||
|
||||
React components inside all Lexical context providers. Used for:
|
||||
- Registering **listeners**, **transforms**, and **commands**
|
||||
- Opening drawers, running side effects
|
||||
|
||||
```ts
|
||||
export const MyClientFeature = createClientFeature({
|
||||
plugins: [MyPlugin],
|
||||
})
|
||||
```
|
||||
|
||||
### Toolbar Groups
|
||||
|
||||
Two display types:
|
||||
|
||||
| Type | Display | Exported helper |
|
||||
|------|---------|-----------------|
|
||||
| `buttons` | Horizontal, icon only | `toolbarFormatGroupWithItems`, `toolbarFeatureButtonsGroupWithItems` |
|
||||
| `dropdown` | Vertical, icon + label | `toolbarAddDropdownGroupWithItems`, `toolbarTextDropdownGroupWithItems` |
|
||||
|
||||
Groups with the same `key` have their items merged.
|
||||
|
||||
### Toolbar Items
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `key` | Unique identifier |
|
||||
| `ChildComponent` | Icon React component |
|
||||
| `Component` | Fully replaces default button (ignores `ChildComponent`/`onSelect`) |
|
||||
| `label` | Shown in dropdown groups; can be a function for i18n |
|
||||
| `onSelect` | Called on click |
|
||||
| `isActive` | Highlight state function |
|
||||
| `isEnabled` | Grays out if `false` |
|
||||
|
||||
Add to `toolbarFixed` or `toolbarInline` (or both):
|
||||
|
||||
```ts
|
||||
export const MyClientFeature = createClientFeature({
|
||||
toolbarFixed: {
|
||||
groups: [
|
||||
toolbarAddDropdownGroupWithItems([{
|
||||
ChildComponent: IconComponent,
|
||||
key: 'myNode',
|
||||
label: ({ i18n }) => i18n.t('lexical:myFeature:label'),
|
||||
onSelect: ({ editor }) => editor.dispatchCommand(INSERT_MYNODE_COMMAND, undefined),
|
||||
isActive: ({ selection }) => { /* ... */ },
|
||||
}]),
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Slash Menu
|
||||
|
||||
Groups + items; groups with the same key are merged.
|
||||
|
||||
```ts
|
||||
export const MyClientFeature = createClientFeature({
|
||||
slashMenu: {
|
||||
groups: [
|
||||
slashMenuBasicGroupWithItems([{
|
||||
Icon: IconComponent,
|
||||
key: 'myNode',
|
||||
keywords: ['myNode', 'myFeature'],
|
||||
label: ({ i18n }) => i18n.t('lexical:myFeature:label'),
|
||||
onSelect: ({ editor }) => editor.dispatchCommand(INSERT_MYNODE_COMMAND, undefined),
|
||||
}]),
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Markdown Transformers (client)
|
||||
|
||||
Client-side transformers trigger when a pattern is **typed** into the editor (live preview). Server-side transformers are used for serialization/deserialization.
|
||||
|
||||
### Providers
|
||||
|
||||
Wrap editor with React context providers:
|
||||
|
||||
```ts
|
||||
export const MyClientFeature = createClientFeature({
|
||||
providers: [TableContext],
|
||||
})
|
||||
```
|
||||
|
||||
## Props & Sanitization
|
||||
|
||||
```ts
|
||||
// Server
|
||||
createServerFeature<UnSanitizedProps, SanitizedProps, UnSanitizedClientProps>({
|
||||
feature: async ({ props }) => {
|
||||
const sanitizedProps = process(props)
|
||||
return { sanitizedServerFeatureProps: sanitizedProps }
|
||||
},
|
||||
})
|
||||
|
||||
// Client
|
||||
createClientFeature<UnSanitizedClientProps, SanitizedClientProps>(({ props }) => {
|
||||
return { sanitizedClientFeatureProps: process(props) }
|
||||
})
|
||||
```
|
||||
|
||||
Client props must be **JSON-serializable** (no functions, Maps, etc.) — they travel over the network from server to client.
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- **Two-file split** is mandatory: `feature.server.ts` registers everything, `feature.client.ts` handles UI
|
||||
- **Never import from raw lexical packages** — use `@payloadcms/richtext-lexical/lexical/*` re-exports
|
||||
- **Try `BlocksFeature` first** — it covers most custom component use cases without a full custom feature
|
||||
- **`featureProviderMap`** is available even before features load — use it to check for optional deps
|
||||
- **Client props must be serializable** — functions/Maps stay server-side only
|
||||
- **Same key = merge** — toolbar groups and slash menu groups with identical keys have items merged
|
||||
- **Node files must not have `'use client'`** — node classes run on both sides
|
||||
- **`graphQLPopulationPromises`** required for sub-field population in GraphQL (REST hooks don't apply there)
|
||||
|
||||
## Related Articles
|
||||
|
||||
- [[wiki/payloadcms/rich-text|Rich Text (Lexical) Overview]]
|
||||
- [[wiki/payloadcms/rich-text-blocks|Rich Text — Blocks (Lexical BlocksFeature)]]
|
||||
- [[wiki/payloadcms/rich-text-converters|Rich Text — Converters]]
|
||||
- [[wiki/payloadcms/rich-text-converting-html|Rich Text — Converting HTML]]
|
||||
- [[wiki/payloadcms/rich-text-converting-markdown|Rich Text — Converting Markdown]]
|
||||
- [[wiki/payloadcms/hooks|PayloadCMS — Hooks]]
|
||||
|
||||
## Sources
|
||||
|
||||
- `raw/rich-text__custom-features.md`
|
||||
- [Payload Docs: Custom Features](https://payloadcms.com/docs/rich-text/custom-features)
|
||||
- [Lexical Docs](https://lexical.dev/docs/intro)
|
||||
Loading…
Add table
Reference in a new issue