vault backup: 2026-05-15 16:43:24

This commit is contained in:
Vadym Samoilenko 2026-05-15 16:43:24 +01:00
parent 60fad12c7a
commit 7f83c117d9
6 changed files with 351 additions and 1 deletions

View file

@ -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 |

View file

@ -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 |

View 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

View 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)