⚠️ Work in progress — createCMS is pre-1.0 and not production-ready (not tested in production). Expect breaking changes.
createCMS

Blocks

Block properties, the eight field types, and functional blocks.

Blocks are the pieces a content tree is built from. A block definition declares a label and a set of typed properties. The same property shape is used by the root and by every child block.

import { defineBlock } from '@createcms/core';

const hero = defineBlock({
  label: 'Hero',
  properties: {
    headline: { type: 'string', required: true, label: 'Headline' },
    align: {
      type: 'select',
      label: 'Alignment',
      options: [
        { label: 'Left', value: 'left' },
        { label: 'Right', value: 'right' },
      ],
    },
  },
});

Property field types

Every property has a type and a label, plus optional required, defaultValue, description, placeholder, and group. There are eight field types:

TypeStored valueNotes
stringstringSingle-line text.
numbernumberNumeric value.
booleanbooleanTrue or false.
datestringISO date string.
richTextstringHTML or rich-text markup.
imagestringAsset reference (object key).
selectstringOne of a fixed set; requires options.
referencestringThe target entry's rootId; requires collection.

Two types carry extra config. select requires an options array of { label, value }, and the stored value is one of those values. reference requires a collection name, and the stored value is the referenced entry's rootId. On the published read path, a reference is inlined to its resolved target.

Nesting

A block is a leaf by default — it holds no children. Set allowChildren: true to make it a container:

const section = defineBlock({
  label: 'Section',
  properties: {},
  allowChildren: true,
});

allowChildren is the coarse gate (can this block contain anything at all?). Which children a container may hold is declared separately, on the collection, in its structure map. Block types are named by their keys in defineCollection, so that map is the only place those names can be type-checked and autocompleted.

Placement rules: structure

structure is keyed by parent block name — or the literal 'root' for the top level. Each entry is one of three mutually exclusive modes:

EntryModeMeaning
{} or { accepts: '*' }openHolds any block (same as no entry).
{ accepts: ['a', 'b'] }whitelistHolds only a and b. Fail-closed: a block added later is rejected until listed.
{ excludes: ['z'] }blacklistHolds anything except z. Fail-open: a block added later is allowed.
const pages = defineCollection({
  label: 'Pages',
  root: pageRoot,
  blocks: { hero, section, featureItem },
  structure: {
    root: { excludes: ['featureItem'] }, // top level: anything except feature items
    section: { accepts: ['featureItem'] }, // a section holds only feature items
  },
});

A concrete accepts list and excludes are mutually exclusive — setting both is a compile error, because the list already says exactly what is allowed. Write accepts: '*' together with excludes only if you want the open base stated explicitly.

To keep a block off the top level, exclude it from the root entry — either root: { excludes: ['featureItem'] } or a whitelist that omits it, root: { accepts: ['hero', 'section'] }.

Block names in structure autocomplete, and a typo is a compile error. The rules come from one source and are enforced in three places: the type checker, the visual editor's drop zones, and the server on createBlock / moveBlock / duplicateBlock (which throws BLOCK_NOT_ALLOWED_IN_PARENT).

Placement is parent-anchored: a container declares what it accepts. "This block may only live inside that one" follows from the containers' accept-lists plus a root rule — there is no per-child allowed parents field.

Editor grouping

Set an optional group on a block to control which category it appears under in the editor's block picker. It is purely presentational — the editor groups blocks by this label; the package never acts on it.

const signupForm = defineBlock({ label: 'Signup Form', group: 'Forms', properties: { /* … */ } });
const hero = defineBlock({ label: 'Hero', group: 'Layout', properties: { /* … */ } });

group is a free-form string. For consistent, autocompleted group names across many blocks, reference a shared as const object:

export const BLOCK_GROUPS = { forms: 'Forms', layout: 'Layout', content: 'Content' } as const;

defineBlock({ label: 'Signup Form', group: BLOCK_GROUPS.forms, properties: { /* … */ } });

The same group hint exists on individual properties — the field-group (fieldset/section) a field is shown under in the property panel. Same rules: free-form string, use a shared as const for consistency.

defineBlock({
  label: 'Page',
  properties: {
    title: { type: 'string', label: 'Title', group: 'Content' },
    metaDescription: { type: 'string', label: 'Meta description', group: 'SEO' },
    canonicalUrl: { type: 'string', label: 'Canonical URL', group: 'SEO' },
  },
});

Functional blocks and events

A block can declare the events it emits (for example a form's submitSuccess). Declaring events requires a stable trackingId property, added with the trackingId() helper. Events live on the block definition, which makes them the single source of truth for analytics and A/B goal tracking.

import { defineBlock, trackingId } from '@createcms/core';

const signupForm = defineBlock({
  label: 'Signup Form',
  properties: {
    ...trackingId(),
    cta: { type: 'string', required: true, label: 'CTA' },
  },
  events: {
    submit: {},
    submitSuccess: { name: 'generate_lead' },
  },
});

Event tracking is wired up by the A/B testing plugin. For exact signatures, see defineBlock.

On this page