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

React

Block rendering, variant, and tracking exports for React.

These exports turn published content into React UI and wire up event tracking. The rendering and variant helpers come from the @createcms/core/react barrel; the tracking helpers come from the @createcms/core/react/tracking subpath (they are not re-exported by the barrel).

Block rendering stays on @createcms/core/react. The headless editor primitive is documented separately: Editor, Editor canvas, and Editor cms adapter.

Rendering

createBlocksMap

Builds a typed map from block type to component. Each component's properties are typed from the collection definition.

function createBlocksMap(collection, components): BlocksMap;
const pageBlocks = createBlocksMap(collections.pages, {
  hero: ({ properties }) => <h1>{properties.headline}</h1>,
});

The returned BlocksMap also carries the collection definition it was built from (typed), so a single object can drive both rendering and an editor (components + schema/placement/grouping) — no separate collection handoff. BlocksRenderer only reads the component map; the bundled collection is there for editor tooling.

BlocksRenderer

The low-level renderer. Renders a block tree with a BlocksMap built by createBlocksMap. Use it when you want to build the map once (to share with editor tooling) and render it in several places.

function BlocksRenderer(props: {
  blocks: BlocksMap;
  tree: BlockTreeNode;
  diff?: BlocksDiffOptions;
  edit?: 'preview';
}): ReactNode;

The optional diff prop turns on diff-aware rendering for an annotated tree from getDiff({ view: 'tree' }): changed blocks get wrapped in a <div data-diff="added | deleted | modified | moved"> element (or your own wrap callback). See Review changes visually.

The optional edit prop selects how the edit prop of every block component is filled: absent → NO_EDIT (no anchors in the HTML); 'preview'data-editor-block / data-editor-field anchors on every block of the tree (referenced trees never get anchors), so an HTML preview can map a click back to a block and a field. edit.active is false in both cases.

createContentRenderer

The convenience factory: pass a collection and a partial component map (components only for the block types you render) and get back a ready ({ tree }) => ReactNode component in one step — no separate createBlocksMap call. This is the recommended entry point. Both paths resolve inline references; this one differs by accepting a partial map.

function createContentRenderer(collection, components): (props: { tree: BlockTreeNode; diff?: BlocksDiffOptions; edit?: 'preview' }) => ReactNode;

BlockComponentProps

The props every block component receives from the renderer map.

type BlockComponentProps = {
  properties: InferBlockProperties; // always an object; pass M = 'raw' to type against the editor's raw store values
  children: ReactNode;
  blockId: string;
  node: BlockTreeNode;
  edit: EditProps;
};

edit carries the editor anchors as plain data — no functions, so the object crosses the server → client component boundary. Outside an editor it is NO_EDIT; with edit="preview" on the renderer it carries real anchors. edit.field has one entry per property key from the block DEFINITION (all-optional blocks still have them), and each entry is OPTIONAL, so {...edit.field.x} is a no-op outside an editor.

type EditAttrs = {
  'data-editor-block'?: string;
  'data-editor-field'?: string;
};

type EditProps = {
  active: boolean; // true only inside an interactive editor canvas
  block: EditAttrs;
  field: { [key: string]: EditAttrs | undefined };
};
import { NO_EDIT, type BlockProps } from '@createcms/core/react/blocks';

export function Hero({ properties, edit }: BlockProps<typeof pages, 'hero'>) {
  return (
    <section {...edit.block}>
      <h1 {...edit.field.headline}>{properties.headline}</h1>
    </section>
  );
}
// Rendering a block by hand (tests, stories): <Hero {...props} edit={NO_EDIT} />

getBlockDiff

Typed accessor for the per-node annotation of a diff tree (getDiff({ view: 'tree' })). Returns null on unchanged nodes and regular (non-diff) trees. Server-safe when imported from @createcms/core/react/blocks — the @createcms/core/react barrel is a 'use client' boundary.

function getBlockDiff(node: BlockTreeNode): BlockDiffAnnotation | null;

diffSegmentsToHtml

Renders the word-level textDiff segments of a richText property change as an HTML string with <ins>/<del> markers, for the same dangerouslySetInnerHTML path you already use for rich text. Style the markers via ins[data-diff-text] / del[data-diff-text]. Tags are never wrapped and deleted tags are dropped, so a formatting-only change (say, bolding a word) renders as the new HTML with no inline highlight. Server-safe when imported from @createcms/core/react/blocks — the @createcms/core/react barrel is a 'use client' boundary.

function diffSegmentsToHtml(segments: TextDiffSegment[]): string;

pickVariant

Server-side variant picker. Returns the tree of the variant matching branchId (or the control), with A/B metadata stripped.

function pickVariant(
  variants: readonly { branchId: string; tree: BlockTreeNode }[],
  branchId: string | null,
  controlBranchId?: string,
): BlockTreeNode | null;

extractBlockEvents

Extracts per-block-type event declarations from a collection's blocks.

function extractBlockEvents(
  blocks: Record<string, AnyBlockDefinition> | undefined,
): Record<string, Record<string, EventDeclaration>>;

useStore

Subscribes a component to a nanostores atom.

function useStore<T>(store: ReadableAtom<T>): T;

Tracking

Imported from @createcms/core/react/tracking (not the @createcms/core/react barrel). See Events & tracking for the model.

createTrackedBlocks

Builds a typed event firer for a collection. useTrackedBlock(blockType) returns a fire narrowed to that block's declared events, so an unknown event name or wrong-typed param is a compile error.

function createTrackedBlocks(collection): {
  useTrackedBlock(blockType): { fire: BlockEventFire };
};

TrackingRuntimeProvider

Supplies the dispatch function and ambient A/B context to every BlockTracker below it.

function TrackingRuntimeProvider(props: { runtime: TrackingRuntime; children: ReactNode }): ReactNode;

BlockTracker

Scopes a block's tracking identity (type, id, trackingId, events) to its children. The renderer adds this around functional blocks automatically.

function BlockTracker(props: BlockTrackingCtx & { children: ReactNode }): ReactNode;

useBlockTrackerRaw

The untyped escape hatch. Returns fire and fireInteraction to dispatch events from inside a tracked block (expectedBlockType is a dev-time label, not a type-narrowing selector). Prefer createTrackedBlocks for compile-time checking.

function useBlockTrackerRaw(expectedBlockType?: string): {
  fire: (name: string, params?: Record<string, string | number | boolean>) => void;
  fireInteraction: (
    name: string,
    interactionId: string,
    params?: Record<string, string | number | boolean>,
  ) => void;
};

TrackedForm

A <form> wrapper that fires an attempt event and a success event around a form action.

function TrackedForm(props: {
  attempt: string;
  success: string;
  action: (formData: FormData) => void | Promise<void>;
  children: ReactNode;
}): ReactNode;

Realtime

Imported from the @createcms/core/react/realtime subpath (which pulls in the optional @upstash/realtime peer — kept out of the main @createcms/core/react barrel). Both hooks ride one shared connection; see Realtime.

RealtimeProvider

Wraps your app in one shared realtime connection that every CMS realtime hook (useNotifications, the A/B useLiveResults) rides. Derives the endpoint + credentials from baseURL.

function RealtimeProvider(props: {
  children: ReactNode;
  baseURL: string; // same value passed to createCMSClient
  maxReconnectAttempts?: number;
}): ReactNode;

useNotifications

Subscribes the current user to their live notification stream over the RealtimeProvider connection and seeds from the notifications.list poll. De-dupes by id; the provider replays anything missed across a reconnect. Realtime-only — requires a mounted RealtimeProvider and notifications enabled (it type-requires client.notifications). client is your typed createCMSClient instance (no cast needed). userId is optional: pass it straight from your auth session (session?.user?.id) — while it's undefined the hook stays poll-only and connects once it resolves (the CMS has no current-user endpoint, so your app supplies the id).

function useNotifications<TActorUser>(
  client: CMSClient,
  options: {
    userId?: string;
    limit?: number;
    withUser?: true;
  },
): {
  notifications: NotificationListItem<TActorUser>[];
  unreadCount: number;
  isLive: boolean;
  refresh: () => void;
};

TActorUser is inferred from your typed client — a partial of your user table's row. With withUser: true, each notification's actorUser (the responsible user) is typed off your config, on both the seed poll and live pushes (the wire event carries actorUser, so the actor shows immediately — no second poll).

const { notifications } = useNotifications(cmsClient, {
  userId: session?.user?.id,
  withUser: true,
});
// notifications[0].actorUser?.name — typed, present live and on poll

Each item carries type, actorId, resourceType, resourceId, collection, and a free-form meta for deep-linking. See createNotificationRouter to turn those into typed links.

createNotificationRouter

Imported from @createcms/core/react (pure — no realtime peer, so it works even without realtime). Maps each notification type to a resolver that builds a deep link (and optional display data) from the item's fields. Each resolver receives meta narrowed to that type; the required fallback keeps routing total for custom and any future type.

function createNotificationRouter<TCMS>(routes: {
  [type]?: (n: TypedNotification<type, TCMS>) => NotificationRoute;
  fallback: (n: NotificationListItem) => NotificationRoute;
}): { resolve: (n: NotificationListItem) => NotificationRoute };

type NotificationRoute = {
  href?: string | null; // null = non-navigable
  label?: string;
  icon?: string;
  group?: string;
};

Pass your CMS type — createNotificationRouter<typeof cms>(…) — to pick up plugin-contributed notification types (and your actorUser shape) with full meta typing.

lib/notification-routes.ts
import { createNotificationRouter } from '@createcms/core/react';
import type { cms } from './cms';

export const notificationRouter = createNotificationRouter<typeof cms>({
  mention: (n) => ({ href: `/threads/${n.meta.threadId}#${n.meta.messageId}`, icon: 'at-sign' }),
  mergeRequestOpened: (n) => ({ href: `/merge-requests/${n.meta.mergeRequestId}` }),
  published: (n) => ({ href: `/${n.collection}/${n.resourceId}`, icon: 'rocket' }),
  // a type from a plugin's `notificationTypes` — `meta` typed off typeof cms:
  abTestWinner: (n) => ({ href: `/experiments/${n.meta.testId}` }),
  fallback: (n) => ({ href: n.resourceId ? `/${n.collection}/${n.resourceId}` : null }),
});

// at render: const { href, icon } = notificationRouter.resolve(notification);

The built-in core types (mention, comment, mergeRequest*, approval*, published, …) come with typed meta out of the box; plugins add their own types via the notificationTypes seam, inferred through typeof cms. To type a custom (or other app-only) notification raised without a plugin, augment NotificationMetaMap:

declare module '@createcms/core/react' {
  interface NotificationMetaMap {
    custom: { kind: 'invoice'; invoiceId: string };
  }
}

On this page