@comet/agent-features 9.0.0-canary-20260527154746
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/package.json +19 -0
- package/rules/coding-guidelines/api-nestjs.instructions.md +107 -0
- package/rules/coding-guidelines/cdn.instructions.md +24 -0
- package/rules/coding-guidelines/general.instructions.md +30 -0
- package/rules/coding-guidelines/git.instructions.md +37 -0
- package/rules/coding-guidelines/kubernetes.instructions.md +59 -0
- package/rules/coding-guidelines/libraries.instructions.md +34 -0
- package/rules/coding-guidelines/naming.instructions.md +39 -0
- package/rules/coding-guidelines/postgresql.instructions.md +40 -0
- package/rules/coding-guidelines/react.instructions.md +102 -0
- package/rules/coding-guidelines/security.instructions.md +44 -0
- package/rules/coding-guidelines/styling.instructions.md +50 -0
- package/rules/coding-guidelines/typescript.instructions.md +50 -0
- package/skills/.gitkeep +0 -0
- package/skills/comet-block/SKILL.md +246 -0
- package/skills/comet-block/references/admin-patterns.md +192 -0
- package/skills/comet-block/references/api-patterns.md +183 -0
- package/skills/comet-block/references/block-loader.md +368 -0
- package/skills/comet-block/references/block-types.md +210 -0
- package/skills/comet-block/references/custom-block-field.md +266 -0
- package/skills/comet-block/references/fixtures.md +436 -0
- package/skills/comet-block/references/image.md +341 -0
- package/skills/comet-block/references/migration.md +597 -0
- package/skills/comet-block/references/registration.md +167 -0
- package/skills/comet-block/references/response-summary.md +102 -0
- package/skills/comet-block/references/rich-text.md +309 -0
- package/skills/comet-block/references/select.md +176 -0
- package/skills/comet-block/references/site-patterns.md +202 -0
- package/skills/comet-mail-react/SKILL.md +539 -0
- package/skills/comet-mail-react/references/components-and-theme.md +395 -0
- package/skills/comet-mail-react/references/layout-patterns.md +315 -0
- package/skills/comet-mail-react/references/styling-and-customization.md +306 -0
- package/skills/comet-minor-update/SKILL.md +191 -0
- package/skills/dev-pm/SKILL.md +100 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Block Registration Patterns
|
|
2
|
+
|
|
3
|
+
"Registering a block" means adding it to a **blocks block** (`createBlocksBlock`) so editors can select and use it in a content area. A newly created block is not available in the admin UI until it is registered in at least one blocks block. Registration must happen consistently across all three layers (API, Admin, Site).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Registration Targets
|
|
8
|
+
|
|
9
|
+
Most Comet projects have a hierarchy of blocks blocks:
|
|
10
|
+
|
|
11
|
+
| Target | Purpose | Typical Includes |
|
|
12
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| **PageContentBlock** | Top-level page content area. Editors build page content by adding blocks here. | All general-purpose blocks, including `columns`, `contentGroup`, `pageTreeIndex`, and any full-width page-level blocks. |
|
|
14
|
+
| **ContentGroupBlock** | A grouped content section with a shared background color or visual wrapper. Used inside `PageContentBlock`. | Same as `PageContentBlock`, minus `contentGroup` (no self-nesting), `pageTreeIndex`, and page-level-only blocks. |
|
|
15
|
+
| **ColumnsContentBlock** | Content inside a single column of a `ColumnsBlock`. More restricted due to limited width. | Reduced set: `accordion`, `anchor`, `richText`, `space`, `heading`, `callToActionList`, `media`. |
|
|
16
|
+
| **AccordionContentBlock** | Content inside an accordion item. Very restricted. | Minimal set: `richText`, `heading`, `space`, `callToActionList`. |
|
|
17
|
+
|
|
18
|
+
Not all projects have all of these. Discover the project's blocks blocks by searching for `createBlocksBlock` usages.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Default Registration Target
|
|
23
|
+
|
|
24
|
+
When the user does not specify where to register a block:
|
|
25
|
+
|
|
26
|
+
1. **Always add to `PageContentBlock`** -- the main content area.
|
|
27
|
+
2. **Also add to `ContentGroupBlock`** if it exists and the block is a general-purpose content block.
|
|
28
|
+
3. **Do not add to `ColumnsContentBlock`** or `AccordionContentBlock` unless explicitly requested.
|
|
29
|
+
|
|
30
|
+
Blocks that should typically **not** be added to `ContentGroupBlock`:
|
|
31
|
+
|
|
32
|
+
- `contentGroup` -- would cause self-nesting
|
|
33
|
+
- `pageTreeIndex` -- page-level navigation block
|
|
34
|
+
- Any full-width or page-level-only block (e.g., a hero block)
|
|
35
|
+
- Any block that is a layout container wrapping other blocks blocks
|
|
36
|
+
|
|
37
|
+
Blocks that should typically **not** be added to `ColumnsContentBlock`:
|
|
38
|
+
|
|
39
|
+
- `teaser`, `featureList`, `columns` -- too wide or complex for column layouts
|
|
40
|
+
- `contentGroup`, `pageTreeIndex` -- page-level blocks
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Consistent Keys Across Layers
|
|
45
|
+
|
|
46
|
+
The key used to register a block in `supportedBlocks` must be **identical across API, Admin, and Site**. The key is **camelCase** and typically matches the block's purpose or name (without "Block").
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// API
|
|
50
|
+
supportedBlocks: { featureList: FeatureListBlock }
|
|
51
|
+
|
|
52
|
+
// Admin
|
|
53
|
+
supportedBlocks: { featureList: FeatureListBlock }
|
|
54
|
+
|
|
55
|
+
// Site
|
|
56
|
+
const supportedBlocks: SupportedBlocks = {
|
|
57
|
+
featureList: (props) => <FeatureListBlock data={props} />,
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A mismatch in keys causes data loss or runtime errors.
|
|
62
|
+
|
|
63
|
+
**Common key conventions:**
|
|
64
|
+
|
|
65
|
+
| Block | Typical Key |
|
|
66
|
+
| --------------------------------- | ------------------ |
|
|
67
|
+
| `StandaloneRichTextBlock` | `richText` |
|
|
68
|
+
| `StandaloneHeadingBlock` | `heading` |
|
|
69
|
+
| `StandaloneMediaBlock` | `media` |
|
|
70
|
+
| `StandaloneCallToActionListBlock` | `callToActionList` |
|
|
71
|
+
| `ColumnsBlock` | `columns` |
|
|
72
|
+
| `ContentGroupBlock` | `contentGroup` |
|
|
73
|
+
| `FeatureListBlock` | `featureList` |
|
|
74
|
+
| `TeaserBlock` | `teaser` |
|
|
75
|
+
| `SpaceBlock` | `space` |
|
|
76
|
+
| `AccordionBlock` | `accordion` |
|
|
77
|
+
| `AnchorBlock` | `anchor` |
|
|
78
|
+
| `ProductListBlock` | `productList` |
|
|
79
|
+
|
|
80
|
+
For new blocks, derive the key from the block name in camelCase without "Block" (e.g., `ProductListBlock` -> `productList`).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Step-by-Step: Registering a Block
|
|
85
|
+
|
|
86
|
+
To register `FeatureListBlock` in `PageContentBlock`:
|
|
87
|
+
|
|
88
|
+
**1. API layer** -- add import and key to `supportedBlocks`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { FeatureListBlock } from "@src/documents/pages/blocks/feature-list.block";
|
|
92
|
+
|
|
93
|
+
export const PageContentBlock = createBlocksBlock(
|
|
94
|
+
{
|
|
95
|
+
supportedBlocks: {
|
|
96
|
+
// ... existing blocks ...
|
|
97
|
+
featureList: FeatureListBlock,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
"PageContent",
|
|
101
|
+
);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**2. Admin layer** -- same key:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { FeatureListBlock } from "@src/documents/pages/blocks/FeatureListBlock";
|
|
108
|
+
|
|
109
|
+
export const PageContentBlock = createBlocksBlock({
|
|
110
|
+
name: "PageContent",
|
|
111
|
+
supportedBlocks: {
|
|
112
|
+
// ... existing blocks ...
|
|
113
|
+
featureList: FeatureListBlock,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**3. Site layer** -- same key, **render function** syntax:
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { FeatureListBlock } from "@src/documents/pages/blocks/FeatureListBlock";
|
|
122
|
+
|
|
123
|
+
const supportedBlocks: SupportedBlocks = {
|
|
124
|
+
// ... existing blocks ...
|
|
125
|
+
featureList: (props) => <FeatureListBlock data={props} />,
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
In the Site layer, the value is a **render function** `(props) => <Component data={props} />`. Some blocks use a `PageContent`-prefixed wrapper for layout purposes (e.g., `heading: (props) => <PageContentStandaloneHeadingBlock data={props} />`). Use the wrapper variant if it exists.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Registering in Multiple Targets
|
|
134
|
+
|
|
135
|
+
When a block should be in both `PageContentBlock` and `ContentGroupBlock`, add it to both. The `ContentGroupBlock` has its own inner blocks block (typically `ContentGroupContentBlock` or `ContentBlock`) defined locally inside the `ContentGroupBlock` file.
|
|
136
|
+
|
|
137
|
+
Repeat the same key in all targets. Repeat the pattern in Admin and Site layers for each target.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## ContentGroupBlock Structure
|
|
142
|
+
|
|
143
|
+
`ContentGroupBlock` is a **composite block** wrapping a blocks block as a child. It is itself registered inside `PageContentBlock`:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
PageContentBlock (blocks block)
|
|
147
|
+
├── contentGroup: ContentGroupBlock (composite block)
|
|
148
|
+
│ ├── backgroundColor (select field)
|
|
149
|
+
│ └── content: ContentGroupContentBlock (blocks block)
|
|
150
|
+
│ ├── accordion, anchor, space, teaser, richText, heading, ...
|
|
151
|
+
│ └── (same as PageContentBlock minus contentGroup, pageTreeIndex, and page-level-only blocks)
|
|
152
|
+
├── columns: ColumnsBlock (composite block)
|
|
153
|
+
│ └── columns[].content: ColumnsContentBlock (blocks block)
|
|
154
|
+
│ └── accordion, anchor, richText, space, heading, callToActionList, media
|
|
155
|
+
├── accordion, richText, heading, media, featureList, teaser, ...
|
|
156
|
+
└── pageTreeIndex, and any other page-level-only blocks
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Discovering Registration Targets
|
|
162
|
+
|
|
163
|
+
1. **Search for `createBlocksBlock`** across the API directory to find all blocks blocks.
|
|
164
|
+
2. **Check `PageContentBlock`** first -- it is the primary registration target.
|
|
165
|
+
3. **Check `ContentGroupBlock`** -- if it exists and has similar blocks, also register there.
|
|
166
|
+
4. **Inspect `ColumnsBlock`** and `AccordionBlock` -- only add blocks there if they suit constrained-width contexts.
|
|
167
|
+
5. **Use the same key** in all targets.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Response Template
|
|
2
|
+
|
|
3
|
+
Use this template for the summary section at the end of **every** block creation or editing response. Include only the sections that apply — omit sections marked "conditional" when they are not relevant.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Template
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
### Blocks created ← omit entire section for pure edit tasks
|
|
13
|
+
- **`BlockName`** — `Layer/path/to/BlockName.block.ts` (API), `Layer/path/to/BlockNameBlock.tsx` (Admin/Site)
|
|
14
|
+
Registered in: `PageContentBlock`, `ContentGroupBlock`
|
|
15
|
+
- **`ListItemBlockName`** — registered in `ListBlockName`
|
|
16
|
+
- **`ListBlockName`** — registered in `PageContentBlock`
|
|
17
|
+
|
|
18
|
+
### Blocks edited ← omit entire section for pure creation tasks
|
|
19
|
+
- **`BlockName`** — added `fieldName` (string), removed `oldField`, changed `image` from `PixelImageBlock` → `DamImageBlock`
|
|
20
|
+
|
|
21
|
+
### Migrations created ← CONDITIONAL: only include if a migration was created
|
|
22
|
+
- **`BlockNameV2Migration`** — `api/src/documents/pages/blocks/migrations/block-name.migration.ts`
|
|
23
|
+
Reason: [brief reason, e.g., "renamed `headline` → `title`", "added required `variant` field"]
|
|
24
|
+
|
|
25
|
+
### Fixtures
|
|
26
|
+
- **`BlockNameFixtureService`** — `api/src/db/fixtures/blocks/block-name-fixture.service.ts`
|
|
27
|
+
- **`ListItemBlockNameFixtureService`** — `api/src/db/fixtures/blocks/list-item-block-name-fixture.service.ts`
|
|
28
|
+
← omit this section if no fixture services were created or modified
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Rules
|
|
34
|
+
|
|
35
|
+
**Created blocks section:**
|
|
36
|
+
|
|
37
|
+
- List every new block file created. Include both the list block and its item block as separate entries.
|
|
38
|
+
- For each block, show its registration target(s). If registered in multiple parents, list all: `registered in \`PageContentBlock\` and \`ContentGroupBlock\``.
|
|
39
|
+
- If the item block is "registered in" a list block, make that explicit: `registered in \`TeaserListBlock\``.
|
|
40
|
+
|
|
41
|
+
**Edited blocks section:**
|
|
42
|
+
|
|
43
|
+
- Describe each change concisely: `added \`fieldName\` (type)`, `removed \`fieldName\``, `changed \`fieldName\` from X → Y`.
|
|
44
|
+
- List each changed block as a separate bullet.
|
|
45
|
+
|
|
46
|
+
**Migrations section (conditional):**
|
|
47
|
+
|
|
48
|
+
- Include only when one or more migration classes were created.
|
|
49
|
+
- State the migration class name and file path.
|
|
50
|
+
- Give a one-line reason (what data shape changed and why).
|
|
51
|
+
|
|
52
|
+
**Fixtures section:**
|
|
53
|
+
|
|
54
|
+
- Include when fixture services were created or meaningfully updated.
|
|
55
|
+
- Omit entirely when no fixture work was done.
|
|
56
|
+
- List each fixture service separately with its file path.
|
|
57
|
+
- If an existing fixture was only wired (not created), note it as "updated `ParentFixtureService` to include `BlockNameFixtureService`".
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Example — Block creation with list block and migration
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
## Summary
|
|
65
|
+
|
|
66
|
+
### Blocks created
|
|
67
|
+
- **`TeaserItemBlock`** — registered in `TeaserListBlock`
|
|
68
|
+
- **`TeaserListBlock`** — registered in `PageContentBlock`, `ContentGroupBlock`
|
|
69
|
+
|
|
70
|
+
### Migrations created
|
|
71
|
+
- **`TeaserListBlockV2Migration`** — `api/src/documents/pages/blocks/migrations/teaser-list-block.migration.ts`
|
|
72
|
+
Reason: added required `variant` field (existing data defaults to `"primary"`)
|
|
73
|
+
|
|
74
|
+
### Fixtures
|
|
75
|
+
- **`TeaserItemBlockFixtureService`** — `api/src/db/fixtures/blocks/teaser-item-block-fixture.service.ts`
|
|
76
|
+
- **`TeaserListBlockFixtureService`** — `api/src/db/fixtures/blocks/teaser-list-block-fixture.service.ts`
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Example — Editing an existing block, no migration
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
## Summary
|
|
85
|
+
|
|
86
|
+
### Blocks edited
|
|
87
|
+
- **`HeroBlock`** — added `subtitle` (string, optional), changed `image` from `PixelImageBlock` → `DamImageBlock`
|
|
88
|
+
|
|
89
|
+
### Fixtures
|
|
90
|
+
- Updated **`HeroBlockFixtureService`** — added `subtitle`, updated `image` field to `DamImageBlock` input format
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Example — Pure creation, no fixtures
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
## Summary
|
|
99
|
+
|
|
100
|
+
### Blocks created
|
|
101
|
+
- **`QuoteBlock`** — registered in `PageContentBlock`
|
|
102
|
+
```
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# RichText Block Rules
|
|
2
|
+
|
|
3
|
+
Detailed rules for creating and configuring RichText blocks in Comet. Load this file when a block contains a rich text field.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Two usage modes:
|
|
10
|
+
|
|
11
|
+
1. **Shared project-wide `RichTextBlock`** — defined once in `common/blocks/` and reused wherever full rich text is needed.
|
|
12
|
+
2. **Scoped `RichTextBlock`** — a locally defined `createRichTextBlock(...)` call inside the composite block file, configured for a specific purpose (single-line eyebrow, heading-only field, etc.).
|
|
13
|
+
|
|
14
|
+
The API block is always the shared one. Per-field configuration is **Admin-only**.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## API Side
|
|
19
|
+
|
|
20
|
+
The API block is project-wide regardless of how many Admin-side scoped variants exist.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// api/src/common/blocks/rich-text.block.ts
|
|
24
|
+
import { createRichTextBlock } from "@comet/cms-api";
|
|
25
|
+
import { LinkBlock } from "@src/common/blocks/link.block";
|
|
26
|
+
|
|
27
|
+
export const RichTextBlock = createRichTextBlock({ link: LinkBlock });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Options: `link` (required — the inline link block) and `indexSearchText` (optional boolean, defaults to `true`).
|
|
31
|
+
|
|
32
|
+
Use as a child block in composite blocks:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { RichTextBlock } from "@src/common/blocks/rich-text.block";
|
|
36
|
+
|
|
37
|
+
class MyBlockData extends BlockData {
|
|
38
|
+
@ChildBlock(RichTextBlock)
|
|
39
|
+
description: BlockDataInterface;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class MyBlockInput extends BlockInput {
|
|
43
|
+
@ChildBlockInput(RichTextBlock)
|
|
44
|
+
description: ExtractBlockInput<typeof RichTextBlock>;
|
|
45
|
+
|
|
46
|
+
transformToBlockData(): MyBlockData {
|
|
47
|
+
return blockInputToData(MyBlockData, this);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Admin Side
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { createRichTextBlock } from "@comet/cms-admin";
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Admin options: `link` (required), `rte` (partial `IRteOptions` — spread over defaults), `minHeight` (pixels, default ~150px), `tags`.
|
|
61
|
+
|
|
62
|
+
### Key `rte` options
|
|
63
|
+
|
|
64
|
+
| Option | Purpose |
|
|
65
|
+
| ------------------- | ----------------------------------------------------------------- |
|
|
66
|
+
| `supports` | Array of toolbar features to enable (see values below) |
|
|
67
|
+
| `maxBlocks` | Max paragraphs — set to `1` for single-line fields |
|
|
68
|
+
| `standardBlockType` | Default block type, e.g. `"header-one"` or `"paragraph-standard"` |
|
|
69
|
+
| `blocktypeMap` | Add custom block types or relabel existing ones |
|
|
70
|
+
|
|
71
|
+
**Available `supports` values:** `"bold"`, `"italic"`, `"underline"`, `"strikethrough"`, `"sub"`, `"sup"`, `"header-one"` through `"header-six"`, `"ordered-list"`, `"unordered-list"`, `"blockquote"`, `"history"`, `"link"`, `"links-remove"`, `"non-breaking-space"`, `"soft-hyphen"`.
|
|
72
|
+
|
|
73
|
+
**Default set** includes all of the above except `"underline"` and `"blockquote"`.
|
|
74
|
+
|
|
75
|
+
**Link rule:** when a `link` block is configured, always include `"link"` and `"links-remove"` in `supports` unless the field is intentionally link-free.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Configuration Patterns
|
|
80
|
+
|
|
81
|
+
### Pattern 1: Shared Full RichTextBlock
|
|
82
|
+
|
|
83
|
+
Defined once per project in `common/blocks/`. No `rte` overrides needed for a standard full-featured editor.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// admin/src/common/blocks/RichTextBlock.tsx
|
|
87
|
+
import { createRichTextBlock } from "@comet/cms-admin";
|
|
88
|
+
import { LinkBlock } from "./LinkBlock";
|
|
89
|
+
|
|
90
|
+
export const RichTextBlock = createRichTextBlock({ link: LinkBlock });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
When projects need custom paragraph variants, configure `standardBlockType` and `blocktypeMap`:
|
|
94
|
+
|
|
95
|
+
> **Note:** `"paragraph-standard"` and `"paragraph-small"` below are **examples only**. The actual block type keys are entirely project-defined. Always check the project's `RichTextBlock` definition to see which custom block types are in use — or whether custom block types are used at all.
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
export const RichTextBlock = createRichTextBlock({
|
|
99
|
+
link: LinkBlock,
|
|
100
|
+
rte: {
|
|
101
|
+
standardBlockType: "paragraph-standard",
|
|
102
|
+
blocktypeMap: {
|
|
103
|
+
"paragraph-standard": {
|
|
104
|
+
label: <FormattedMessage id="richTextBlock.paragraphStandard" defaultMessage="Paragraph Standard" />,
|
|
105
|
+
},
|
|
106
|
+
"paragraph-small": {
|
|
107
|
+
label: <FormattedMessage id="richTextBlock.paragraphSmall" defaultMessage="Paragraph Small" />,
|
|
108
|
+
renderConfig: {
|
|
109
|
+
element: (props) => <Typography paragraph variant="body2" {...props} />,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Pattern 2: Single-Line Inline Field
|
|
118
|
+
|
|
119
|
+
For eyebrows, subtitles, captions — any field that should be a single line of formatted text.
|
|
120
|
+
|
|
121
|
+
Key settings:
|
|
122
|
+
|
|
123
|
+
- `maxBlocks: 1` — prevents Enter from creating new paragraphs
|
|
124
|
+
- `minHeight: 0` — compact editor height
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
const DescriptionRichTextBlock = createRichTextBlock({
|
|
128
|
+
link: LinkBlock,
|
|
129
|
+
rte: {
|
|
130
|
+
maxBlocks: 1,
|
|
131
|
+
supports: ["bold", "italic", "sub", "sup", "non-breaking-space", "soft-hyphen", "link", "links-remove"],
|
|
132
|
+
},
|
|
133
|
+
minHeight: 0,
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Pattern 3: Heading-Only Field
|
|
138
|
+
|
|
139
|
+
For single-line heading/title fields where the user selects a heading level from a dropdown.
|
|
140
|
+
|
|
141
|
+
Key settings:
|
|
142
|
+
|
|
143
|
+
- `maxBlocks: 1` + `minHeight: 0` — single compact line
|
|
144
|
+
- `standardBlockType: "header-one"` — new content defaults to heading 1
|
|
145
|
+
- `blocktypeMap` — only add if the project uses custom heading label names. **Check existing RTE blocks in the project** to see how heading levels are labelled and match that convention. If no custom labels are used, omit `blocktypeMap`.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const HeadlineRichTextBlock = createRichTextBlock({
|
|
149
|
+
link: LinkBlock,
|
|
150
|
+
rte: {
|
|
151
|
+
maxBlocks: 1,
|
|
152
|
+
supports: [
|
|
153
|
+
"header-one",
|
|
154
|
+
"header-two",
|
|
155
|
+
"header-three",
|
|
156
|
+
"header-four",
|
|
157
|
+
"header-five",
|
|
158
|
+
"header-six",
|
|
159
|
+
"bold",
|
|
160
|
+
"italic",
|
|
161
|
+
"sub",
|
|
162
|
+
"sup",
|
|
163
|
+
"non-breaking-space",
|
|
164
|
+
"soft-hyphen",
|
|
165
|
+
],
|
|
166
|
+
standardBlockType: "header-one",
|
|
167
|
+
},
|
|
168
|
+
minHeight: 0,
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Pattern 4: Limited Feature Set
|
|
173
|
+
|
|
174
|
+
When the full RTE is too powerful for the context but multiline content is still needed.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
const BaseRichTextBlock = createRichTextBlock({
|
|
178
|
+
link: ExternalLinkBlock,
|
|
179
|
+
rte: {
|
|
180
|
+
supports: ["bold", "italic", "header-one", "header-two", "header-three", "link", "links-remove"],
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## `blocktypeMap` Patterns
|
|
188
|
+
|
|
189
|
+
### Adding Custom Block Types
|
|
190
|
+
|
|
191
|
+
Custom block types are arbitrary strings outside Draft.js defaults. Define them in `blocktypeMap` and set `standardBlockType` if the custom type should be the default.
|
|
192
|
+
|
|
193
|
+
> **Note:** `"paragraph-standard"` and `"paragraph-small"` below are **examples only**. Block type keys are entirely project-defined — always look at the project's `RichTextBlock` definition to see what custom types are actually configured.
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
blocktypeMap: {
|
|
197
|
+
"paragraph-standard": {
|
|
198
|
+
label: <FormattedMessage id="richTextBlock.paragraphStandard" defaultMessage="Paragraph Standard" />,
|
|
199
|
+
},
|
|
200
|
+
"paragraph-small": {
|
|
201
|
+
label: <FormattedMessage id="richTextBlock.paragraphSmall" defaultMessage="Paragraph Small" />,
|
|
202
|
+
renderConfig: {
|
|
203
|
+
element: (props) => <Typography paragraph variant="body2" {...props} />,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
When `standardBlockType` is set to something other than `"unstyled"`, the `"unstyled"` block type is hidden from the dropdown.
|
|
210
|
+
|
|
211
|
+
### Relabeling Existing Block Types
|
|
212
|
+
|
|
213
|
+
Override just `label` to rename existing types without changing behavior:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
blocktypeMap: {
|
|
217
|
+
"header-one": {
|
|
218
|
+
label: <FormattedMessage id="headingBlock.size600" defaultMessage="Size 600" />,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Naming Conventions
|
|
226
|
+
|
|
227
|
+
**Shared RichTextBlock:**
|
|
228
|
+
|
|
229
|
+
- API: `api/src/common/blocks/rich-text.block.ts`
|
|
230
|
+
- Admin: `admin/src/common/blocks/RichTextBlock.tsx`
|
|
231
|
+
- Site: `site/src/common/blocks/RichTextBlock.tsx`
|
|
232
|
+
- Export name: `RichTextBlock`
|
|
233
|
+
|
|
234
|
+
**Scoped RichTextBlock:** name descriptively as `{Purpose}RichTextBlock` (e.g., `EyebrowRichTextBlock`, `HeadlineRichTextBlock`, `DescriptionRichTextBlock`). Define as a non-exported `const` inside the composite block file:
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
// Inside admin/src/documents/pages/blocks/TeaserItemBlock.tsx
|
|
238
|
+
|
|
239
|
+
const DescriptionRichTextBlock = createRichTextBlock({
|
|
240
|
+
link: LinkBlock,
|
|
241
|
+
rte: { maxBlocks: 1, supports: ["bold", "italic", "sub", "sup", "non-breaking-space", "soft-hyphen", "link", "links-remove"] },
|
|
242
|
+
minHeight: 0,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
export const TeaserItemBlock = createCompositeBlock({
|
|
246
|
+
name: "TeaserItem",
|
|
247
|
+
blocks: {
|
|
248
|
+
description: {
|
|
249
|
+
block: DescriptionRichTextBlock,
|
|
250
|
+
title: <FormattedMessage id="teaserItemBlock.description" defaultMessage="Description" />,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Site Side
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
import { hasRichTextBlockContent, type PropsWithData, withPreview } from "@comet/site-nextjs";
|
|
262
|
+
import { type RichTextBlockData } from "@src/blocks.generated";
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Use `hasRichTextBlockContent` to conditionally render:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
{
|
|
269
|
+
hasRichTextBlockContent(description) && <RichTextBlock data={description} />;
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
When custom block types are added in the Admin, add matching entries in the site's `blocks` renderer map. The keys must exactly match what is defined in the project's Admin-side `RichTextBlock` configuration — `"paragraph-standard"` and `"paragraph-small"` below are **examples only**:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
blocks: {
|
|
277
|
+
"paragraph-standard": createTextBlockRenderFn({ bottomSpacing: true }),
|
|
278
|
+
"paragraph-small": createTextBlockRenderFn({ variant: "paragraph200", bottomSpacing: true }),
|
|
279
|
+
},
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Do **not** wrap `RichTextBlock` in a `Typography` component — the block already defines its own typography through its renderers; wrapping produces conflicting styles.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Decision Guide
|
|
287
|
+
|
|
288
|
+
| Scenario | Use Shared | Create Scoped |
|
|
289
|
+
| ------------------------------------------------------------- | ---------- | -------------------------- |
|
|
290
|
+
| General content area with full formatting | Yes | No |
|
|
291
|
+
| Short description or subtitle (single-line, basic formatting) | No | Yes — single-line pattern |
|
|
292
|
+
| Heading with size dropdown | No | Yes — heading-only pattern |
|
|
293
|
+
| Eyebrow text | No | Yes — single-line pattern |
|
|
294
|
+
| Email campaign with different link block | No | Yes — different link block |
|
|
295
|
+
| Content area with fewer features than default | No | Yes — limited feature set |
|
|
296
|
+
|
|
297
|
+
**Rule of thumb:** if the field needs `maxBlocks`, a custom `supports` set, a custom `standardBlockType`, or a different `link` block — create a scoped RichTextBlock.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Common Pitfalls
|
|
302
|
+
|
|
303
|
+
1. **Forgetting `minHeight: 0`** with `maxBlocks: 1` — editor area is unnecessarily tall for a single-line field.
|
|
304
|
+
2. **Adding custom block types in Admin without updating the site** — site renderers must include all custom block type keys or content using those types renders as nothing.
|
|
305
|
+
3. **Setting `standardBlockType` to a custom type without defining it in `blocktypeMap`** — the dropdown label renders incorrectly.
|
|
306
|
+
4. **Trying to customize the API block per-field** — the API block is shared; all per-field customization is Admin-only.
|
|
307
|
+
5. **Exporting scoped RichTextBlocks** — they must remain private (`const` without `export`) in the composite block file.
|
|
308
|
+
6. **Omitting `"link"` and `"links-remove"` when a link block is configured** — editors cannot add or remove links without these in `supports`.
|
|
309
|
+
7. **Wrapping `RichTextBlock` in `Typography` on the site** — produces double-wrapped, incorrectly styled output.
|