name: comet-block description: Creates and edits Comet blocks (API, Admin, Site) from natural-language prompts, including block fixture services. Use when the user asks to create a new block, edit an existing block, add/remove/change fields or child blocks, scaffold block files, or create block fixtures in a Comet project.
Comet Block Skill
Table of Contents
- When to use
- Workflow routing
- Step 1 — Parse the prompt
- Step 2 — Discover the project
- Editing workflow ← follow this for edits
- Step 3 — Create the API block ← creation only
- Step 4 — Create the Admin block ← creation only
- Step 5 — Create the Site block ← creation only
- Step 6 — Register the block
- Step 7 — Create block fixtures
- Naming conventions
- Code generation
- Cross-references
When to use
- Creating a new Comet block from a natural-language description.
- Scaffolding block files across API, Admin, and Site layers.
- Adding, removing, or changing fields or child blocks in an existing block.
- Changing enum values, field types, or property names in an existing block.
- Creating block fixture services for seeding development databases.
Workflow routing
User request
│
├─ Creating a new block? → Steps 1 → 2 → 3 → 4 → 5 → 6 → 7
│
└─ Editing an existing block? → Steps 1 → 2 → Editing workflow → 7
Step 1 — Parse the prompt
Extract from the user's request:
- Block name — derive PascalCase for Admin/Site (
TeaserItemBlock) and kebab-case for API (teaser-item.block.ts). See Naming conventions. - Properties — for each property determine its type. Common types:
- String — plain text (
title,label). - Number — numeric with min/max (
overlay). - Boolean — toggle/switch (
showOverlay). - Numeric select — fixed numeric options. Use
@IsInt()+@BlockField()in the API (nottype: "enum"); usecreateCompositeBlockSelectFieldwith number options in Admin. See select.md. - Enum/select — fixed string values (
variant,alignment). See select.md. - RichText — formatted text. Choose shared
RichTextBlockor a scoped inline one. See rich-text.md. - Image — choose
DamImageBlock,PixelImageBlock,SvgImageBlock, or a project-specificMediaBlock. See image.md. - Child block — any other existing block used as a property.
- List — multiple instances of a child block; requires a list block + item block pair. See block-types.md.
- String — plain text (
- Registration target — where the block is added. Default:
PageContentBlock(andContentGroupBlockif it exists). Ask if unclear. - Ambiguities — if "image" is mentioned without specifics, or a referenced block may not exist, ask before proceeding.
For block type decision guidance, see block-types.md.
Step 2 — Discover the project
Before generating any code:
- Locate the
apiandadmindirectories. - Locate the site package or packages by their dependencies, not the folder name.
siteis only the conventional name — a project may name the package differently or have several. Apackage.json(outsidenode_modules) that depends on@comet/site-nextjsor@comet/site-reactis certainly a site package, since site blocks render through these (withPreview,PropsWithData):
A frontend package can also exist without those dependencies — for example one that renders blocks for email withgrep -rl --include='package.json' --exclude-dir=node_modules -E '"@comet/site-(nextjs|react)"' . | xargs -n1 dirname@comet/mail-react. Skip the site steps only if you find neither a site package nor another frontend package. - Find existing blocks directories — typically
src/documents/pages/blocks/. Some shared blocks live incommon/blocks/or similar. Check both. - Verify referenced blocks exist — search for any blocks named in the prompt (e.g.,
HeadingBlock,LinkBlock) in all layers and note their import paths. - Find registration targets — search for
createBlocksBlockusages to locatePageContentBlock,ContentGroupBlock, or other targets. Note file paths in all layers.
In the later steps and the reference files, example paths written as site/… mean the site package's root and @src/… means its src/ directory. They show the conventional layout — substitute the actual package folder found here.
Editing workflow
Use this instead of Steps 3–5 when modifying an existing block.
Classify each change
| Change type | Description |
|---|---|
| Add field/child block | A new property on the block. |
| Remove field/child block | An existing property is deleted. |
| Change field type | One type replaces another (e.g., string → RichText). |
| Change enum values | Options added to or removed from a select field. |
| Rename field | Property keeps its type but gets a new name. |
A single request may involve multiple change types — classify each independently.
Determine if a migration is needed
Ask the user before creating a migration if they haven't mentioned it. Explain the old vs new data shape and confirm.
Migration IS needed:
- Adding a required field (existing data lacks it).
- Changing a field's type (old data has the old shape).
- Removing a field (recommended to keep data clean).
- Renaming a field (old data uses the old name).
Migration is NOT needed:
- Adding an optional/nullable field (
@IsUndefinable()+@BlockField({ nullable: true })). - Adding a new supported block to a
BlocksBlockorOneOfBlock. - Adding new enum values (existing data unaffected).
When in doubt, create a migration — it is always the safer choice. See migration.md for the full decision matrix, class template, and annotated examples.
Apply changes in order
- API block — update
BlockDataandBlockInputclasses, decorators, validators. - Migration — create and register if needed. Update
createBlockthird argument to the options object. See migration.md. - Admin block — update the
blocksobject: add/remove entries, labels, options. - Site block (if exists) — update destructured fields and rendered output.
- Block fixture (if exists) — update
generateBlockInput()to match changes. See fixtures.md. - Verify consistency — confirm every property key name matches across all three layers.
Step 3 — Create the API block
File: {block-name}.block.ts (kebab-case). Place in the blocks directory found in Step 2.
Key patterns:
BlockDatauses@BlockField()for fields,@ChildBlock(X)for child blocks.BlockInputuses validators +@ChildBlockInput(X)for child blocks; implementtransformToBlockData()withinputToData.- Export with
createBlock(BlockData, BlockInput, "BlockName"). - Enums require
@BlockField({ type: "enum", enum: MyEnum })— never usetype: "enum"for numeric options. - For list blocks: create the item block first, then
createListBlock({ block: ItemBlock }, "MyList").
For field decorators, validator reference, savability rules, and complete examples, see api-patterns.md.
Step 4 — Create the Admin block
File: {BlockName}Block.tsx (PascalCase). Place in the blocks directory found in Step 2.
Key patterns:
- Use
createCompositeBlockwith ablocksobject mapping property names to block configs. - Helper functions:
createCompositeBlockTextField,createCompositeBlockSelectField,createCompositeBlockSwitchField. fullWidthdefaults totruein text and select helpers — omit it explicitly.- All user-facing strings use
FormattedMessagefromreact-intl. Follow ID convention:{blockName}Block.{field}. - Set
BlockCategorywhen the block is used inside a blocks block. - Set
previewContentin the override callback for meaningful block list previews. hiddenInSubrouterule: When the composite contains sub-route blocks (list, blocks-block, one-of), sethiddenInSubroute: trueon every sibling entry that is not a sub-route block. Never set it on sub-route block entries themselves.labelvstitle: when the helper has alabel, omittitleon the block entry. When it has nolabel, providetitleon the entry. They are mutually exclusive.- For list blocks: use
createListBlockfrom@comet/cms-adminwithname,displayName,block,itemName,itemsName.
For the full helper API, BlockCategory enum, and complete examples, see admin-patterns.md.
Step 5 — Create the Site block (if site exists)
File: {BlockName}Block.tsx (PascalCase). Place in the blocks directory found in Step 2.
Key patterns:
- Wrap with
withPreview(Component, { label: "BlockName" }). - Type props as
PropsWithData<{BlockName}BlockData>— import from@src/blocks.generated. - Use
hasRichTextBlockContentguard before rendering RichText. Never wrapRichTextBlockin aTypographycomponent. DamImageBlockis not exported from@comet/site-nextjs— use the project-specific wrapper. See image.md.- Always validate
aspectRatioagainstallowedImageAspectRatiosin the API config. See image.md. - The
supportedBlocksobject (forBlocksBlock/PageContentsite wrapper) must be defined at module level, not inside the component.
For withPreview, OneOfBlock/OptionalBlock, and complete site examples, see site-patterns.md.
Step 6 — Register the block
Add the new block to the registration target in all three layers using identical camelCase key names.
Default target: PageContentBlock. If ContentGroupBlock exists and mirrors PageContentBlock supported blocks, also register there.
// API & Admin: supportedBlocks object inside createBlocksBlock
myBlock: MyBlock,
// Site: supportedBlocks object
myBlock: (props) => <MyBlock data={props} />,
For the target hierarchy, multiple targets, and edge cases, see registration.md.
Step 7 — Create block fixtures
Create a fixture service that generates realistic seed data for the new block.
- Create the fixture service matching the block type (composite, list, blocks-block, one-of).
- Wire into the parent block's fixture. If the parent has no fixture, ask the user before creating one.
- Register in
fixtures.module.tsas a provider. - For nested list blocks: create the list item fixture service first, then the parent composite fixture which injects and calls it.
For faker guidelines, patterns by block type, and full examples, see fixtures.md.
Naming conventions
| Concept | Convention | Example |
|---|---|---|
| API file name | kebab-case .block.ts |
product-card.block.ts |
| Admin / Site file name | PascalCase Block.tsx |
ProductCardBlock.tsx |
| Export variable | PascalCase + "Block" | ProductCardBlock |
Name string (createBlock etc.) |
PascalCase, no "Block" | "ProductCard" |
API BlockData class |
PascalCase + "BlockData" | ProductCardBlockData |
API BlockInput class |
PascalCase + "BlockInput" | ProductCardBlockInput |
| Registration key | camelCase, no "Block" | productCard |
| Child block property | camelCase | image, callToActionList |
FormattedMessage ID |
camelCase block + field | productCardBlock.headline |
The name parameter in createBlock, createCompositeBlock, createListBlock, createBlocksBlock, and createOneOfBlock is PascalCase without a "Block" suffix and must be identical across all layers.
Cross-references
| Topic | File |
|---|---|
| Block type overview and decision guide | block-types.md |
| API decorator patterns, validators, savability | api-patterns.md |
Admin helpers, BlockCategory, hiddenInSubroute |
admin-patterns.md |
Site withPreview, rendering patterns |
site-patterns.md |
| Registration targets, keys, edge cases | registration.md |
| RichText configuration, scoped vs shared | rich-text.md |
| Enum/select patterns, numeric options, multi-select | select.md |
| Image block selection and site wrappers | image.md |
| Migration decision matrix, class template, examples | migration.md |
| Block fixture patterns and faker guidelines | fixtures.md |
| Block loaders for server-side data fetching | block-loader.md |
| Custom block fields for entity selection | custom-block-field.md |
| Response template for creation/editing output | response-summary.md |