Content Blocks
How to build, configure, and extend blocks
Blocks are the visual building units of any content entry whose type has Supports Blocks enabled. Each block maps directly to an Astro component. Its props are configured through a schema file that defines what controls appear in the editor. Adding a new block to the CMS is a matter of creating a schema file; no additional wiring is required. Check out the builder that powers content blocks:
How the Block Builder Works
-
Experiment with different themes
-
Edit each prop with controls
-
Copy the generated code
When an editor opens a content entry that supports blocks, the builder renders two things side by side: a live preview of the current block stack via ComponentMap, and an editor panel that shows controls for the selected block. Blocks can be added from the block picker, reordered by dragging, duplicated, and deleted. All changes are debounced and saved automatically.
Each block in the stack is stored as a record containing its type, ordered position, a props object, and an optional children array for blocks that support nesting. The editor panel populates from the block’s schema.
Block Schema
Every block is defined by a schema file that exports two named objects:
controls: Defines the editor controls shown for the block.defaults: Defines the initial state of a block when it is first added to the entry.
export const controls = { ... }export const defaults = { ... }controls
The controls object must have a props property, and an optional children property, if the block supports nesting. Each key in props maps to a prop on the component. The kind of each control determines what input renders in the editor panel. You can use the following kinds:
itemGroups functions for dynamically populated options.wrapInCard to visually wrap the group in a card.min and max constraints.Flex, Card, or Grid. Supports allowed to restrict which block types can be nested, min and max child counts, and omitControls to hide specific controls per child block type.defaults
Defines the initial state of a block when it is first added to the entry. Accepts a props object with default prop values, an optional children array for pre-populated child blocks, and an optional documentation link shown in the editor panel. Here is an example of a Card block schema:
import type { CardProps } from 'webcoreui/astro'
import { getClassNameControl } from '@webcore/utils/getControls'import type { ControlOptions, DefaultOptions } from '@webcore/types'
export const controls: ControlOptions<CardProps> = { props: { // Handles the `title` prop on the component title: { kind: 'input', props: { label: 'Title', placeholder: 'Set an optional title for the card' } },
// Handles the `secondary` prop on the component secondary: { kind: 'input', input: 'Checkbox', props: { label: 'Secondary', subText: 'Add secondary style' } } }, children: { kind: 'children', label: 'Card Content (Children)' }}
export const defaults: DefaultOptions<CardProps> = { documentation: '/docs/card', props: { title: 'Card title' }, children: [{ id: crypto.randomUUID(), type: 'HTML', props: { html: 'Your card content.' } }]}controls and defaults are generic types. This prevents you from accidentally defining props that don’t exist on your component. Block Categories
Blocks are organised into categories based on which subfolder their schema file lives in. The picker groups blocks by category, and the order of categories is controlled by blockSelectorCategoryOrder in webcore.config.ts. The CMS ships with the following categories:
| Category | Schemas located in |
|---|---|
Application | webcore/config/controls/Application |
Blocks | webcore/config/controls/Blocks |
Components | webcore/config/controls/Components |
E-Commerce | webcore/config/controls/E-Commerce |
Marketing | webcore/config/controls/Marketing |
blockSelectorCategoryOrder appear after the listed ones, sorted alphabetically. Dynamic Schema Controls
Some blocks require controls populated with live data rather than static options. This is handled through schema API endpoints in pages/api/schema. The CMS ships with the following dynamic schemas:
| Schema | Used by Block | Description |
|---|---|---|
schema/avatar | Avatar | Populates the avatar image picker from available avatar images. |
schema/image | Image | Populates the image picker from your public/img folder. |
schema/reference | Reference | Populates the entry selector with all published entries, grouped by content type. |
To add dynamic data to your own block’s controls, create a new API endpoint under pages/api/schema and reference it in your control using an async itemGroups function, the same pattern used by getImageSelectControl().
Adding a Custom Block
Adding your own block to the CMS is straightforward and can be done by following these steps:
-
Create a schema file in the appropriate category folder under
webcore/config/controls. Place it in a new subfolder to create a new category, or in an existing one to join an existing group. -
Define
controlsanddefaultsin the schema file, typed to your component’s props interface. -
Register the type in
src/components/ComponentMap/componentMap.tsby adding your component name to theComponentTypesunion:
export type ComponentTypes = | 'Card' | 'YourComponent' // add your component here // ...This keeps TypeScript happy across the block builder and ComponentMap. Without this step, the type system will flag usages of your new block.
- Optionally update
blockSelectorCategoryOrderinwebcore.config.tsif you created a new category and want it to appear at a specific position in the picker:
export const blockSelectorCategoryOrder = [ 'Favourites', // your new category at the top 'Components', 'Blocks']Once the schema file exists and the type is registered, the block appears in the picker immediately with your new controls.
Helper Controls
A set of reusable control factories is available in @webcore/utils/getControls.ts to cover common patterns. Use these instead of writing the same control shapes by hand:
| Helper | Description |
|---|---|
getClassNameControl() | A text input for an optional CSS class name. |
getThemeSelectControl() | A select dropdown with the available WebcoreUI theme options. |
getAvatarSelectControl() | A searchable select populated from your avatar images. |
getAvatarSelectListControl() | A repeatable list version of the avatar select. |
getSingleAvatarGroupControl() | A group control for a single avatar with image, alt, and size. |
getIconSelectControl() | A select for available WebcoreUI icon types. |
getLinkTargetControl() | A select for HTML anchor target values (_blank, _self, etc). |
getImageSelectControl() | A searchable select populated from your public/img folder. |
getImageGroupControl() | A group control with src, alt, width, height, and image display options. |
getLogoGroupControl() | An optional group control for a logo image with url, alt, width, height. |
getButtonGroupControl() | A group control for a single button with text and icon. |
getButtonsListControl() | A repeatable list of button groups. |
getResponsiveControl() | A group control with breakpoint-specific values (default, xs, sm, md, lg). |
getBadgeGroupControl() | An optional group control for badge text and icon. |
getRibbonGroupControl() | An optional group control for a ribbon label. |
getGradientRibbonGroupControl() | An optional group control for a gradient ribbon with label, text and background colors. |
getCardGroupControl() | An optional group control for card settings. Accepts additional card props. |
getCardWithSeparatorControl() | A separator followed by card setting controls. |
getRatingGroupControl() | A group control for rating settings. Can be marked optional. Accepts additional props. |
getSecondaryStyleControl() | A checkbox control for applying a secondary style. |
Images placed in public/img are automatically available in getImageSelectControl() and getImageGroupControl(), no additional configuration is needed. The image manifest is regenerated on every dev server start and production build. A set of utility functions is also available for composing and transforming controls when building schemas:
omitControls(): Returns a copy of a controls record with the specified keys removed. Useful when extending a shared control set but excluding props that are not relevant to a specific block.pickControls(): Returns a new controls record containing only the specified keys. The inverse ofomitControls. Use when you want a strict subset of an existing control set.mergeControls(): Merges two controls records into one. Keys inboverride keys inaif they conflict.overrideControls(): Returns a copy of a controls record with specific controls replaced entirely by the provided overrides. Use when you need to swap out a control definition wholesale.overrideControlDefaults(): Returns a copy of a controls record with the default prop values updated. For checkbox controls, the value is applied as checked. For all other controls, it is applied as value. Use when reusing a shared control set but with different starting values.