Components & Schema
The core idea of StormyCMS: define your components once, in code, and both your site and your editor use them. This page explains how the shared component schema works.
The Export Map
Your project keeps a single "CMS export map" — a default export mapping component names to CMS-registered React components. In the boilerplate this is packages/components/index.ts:
import { Outlet, withCMS, type CMSExportsMap } from '@stormycms/react/cms';import { Heading } from './src/heading';import { Text } from './src/text';
export default { Outlet, // Built-in: marks where page content renders inside a layout
Heading: withCMS( { export: 'Heading', label: 'Heading', fields: [ { label: 'Text', prop: 'text', field: 'input', required: true, defaultValue: 'Heading' }, { label: 'Level', prop: 'level', field: 'select', options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], required: false, defaultValue: 'h2', }, ], noChildren: true, }, Heading, ),
Text: withCMS( { export: 'Text', label: 'Text', fields: [ { label: 'Content', prop: 'content', field: 'textarea', required: true, defaultValue: 'Add body copy.' }, ], noChildren: true, }, Text, ),};Both apps import this same map:
- The public site passes it to
ComponentTree/LayoutTreeto render stored pages. - The admin editor reads the attached metadata (
label,fields, placement rules) to build its insertion palette and edit forms — and uses the components themselves for live previews.
When the CMS stores a component named "Heading", rendering works because "Heading" is a key in this map. Names that don't match any key are skipped.
withCMS
withCMS(cmsProps, Component) wraps a React component and attaches CMS metadata:
| Property | Type | Description |
|---|---|---|
export | string | Must match the component's key in the export map |
label | string | Human-friendly name shown in the editor's component picker |
fields | CMSField[] | The editable form fields (below) |
noChildren | boolean | Set true for leaf components (headings, images). Default false. |
validChildren | string[]? | Restrict which components may be nested inside this one |
validParents | string[]? | Restrict which components this one may be nested inside |
layoutOnly | boolean | Only available in the layout editor (e.g. regions, Outlet). Default false. |
editable | boolean | Whether the component shows an edit form. Default true. |
unique | boolean | Only one instance allowed per parent (e.g. a header region). Default false. |
Fields
Each entry in fields describes one editable prop:
{ label: 'Level', // Form label in the editor prop: 'level', // The React prop it populates field: 'select', // Control type options: ['h1', 'h2'], // For 'select' and 'radio' only required: false, defaultValue: 'h2',}Field types
field | Editor control | Value |
|---|---|---|
input | Single-line text | string |
textarea | Multi-line text | string |
markdown | Markdown editor | string (markdown) |
checkbox | Checkbox | boolean |
dateTime | Date/time picker | string |
select | Dropdown (requires options) | string |
radio | Radio group (requires options) | string |
image | Media picker (uses your S3-backed media library) | ImageAsset |
textList | Repeatable text inputs | string[] |
numberList | Repeatable number inputs | number[] |
checkboxList | Repeatable checkboxes | string[] |
textareaList | Repeatable textareas | string[] |
keyValueList | Repeatable key/value pairs | record |
list | Repeatable list of typed fields | unknown[] |
complexList | Repeatable sub-form | unknown[] |
noop | No control; computed via a provided fn | any |
For list, specify fieldTypes: CMSFieldType[] to constrain the allowed fields inside each item. For complexList, specify subFields: CMSField[] for a full nested form.
The image field and ImageAsset
The image field renders a media picker in the admin. The value stored on the component is an ImageAsset:
type ImageAsset = { src: string; alt?: string; mediaId?: string; name?: string; tags?: string[];};The boilerplate admin uploads files to your S3 bucket via POST /api/media, creates a Stormy CMS Media record, and stores the resulting URL as src, the media ID as mediaId, and any alt text/tags as alt/tags. Components receive the same ImageAsset shape on the public site, so they can render <img src={image.src} alt={image.alt} /> directly.
Placement Rules in Practice
From the boilerplate's Section component:
Section: withCMS( { export: 'Section', label: 'Section', fields: [{ label: 'Label', prop: 'label', field: 'input', required: false, defaultValue: '' }], validChildren: ['Heading', 'Text', 'RichText', 'Image', 'CallToAction', 'Group', 'Card', 'CardGroup', 'Hero'], }, Section,),The editor will only offer those child components inside a Section. Combine validChildren, validParents, unique, and layoutOnly to encode your design system's composition rules so editors can't create invalid structures.
Layouts and the Outlet
Layout-only components (like the boilerplate's Region, RegionHeader, RegionFooter) build page chrome. The built-in Outlet component (exported by @stormycms/react/cms) marks where page content renders inside a layout. Include it in your export map and place it in your layouts via the layout editor.
Layouts can nest: a child layout (say, "Blog") sets parentLayoutId to the site-wide layout, and buildLayoutTree assembles the chain at render time.
Generating a Starting Schema
Already have components? Let the CLI infer a first draft:
stormy generate --components ./packages/components/src --output ./packages/components/generated.tsGeneration maps prop types to sensible field controls. Review the output and refine labels, defaults, and placement rules — that's expected workflow, not a shortcoming. The boilerplate's own export map started generated and was then adapted by hand.
Why This Matters for AI Agents
Because the schema is TypeScript in your repository, an AI coding agent can:
- Create a new React component file
- Register it in the export map with appropriate fields
- Use the GraphQL API to publish pages that use it
…all without logging into any external system. The full content model is inspectable with nothing but file reads.
Next Steps
- Boilerplate Tour - Where these pieces live in a generated project
- @stormycms/react - Rendering API details
- CLI Reference -
stormy generateoptions
Last updated: 7/9/26, 6:42 AM