Block Development Guide

Build custom blocks for your headless Cmssy site - defineBlock, fields, the block component, and context.

June 29, 2026

Block anatomy

A Cmssy block lives in your own Next.js repo as a folder under blocks/:

  • block.ts — declares the block with defineBlock + fields (its type, label, and editable schema)
  • Hero.tsx — the React component that renders the content

Register the block in cmssy/blocks.ts. The editor reads each block's schema over the SDK bridge, so adding a block to a page renders an editor form from your fields.


Define a block

// blocks/hero/block.ts
import { defineBlock, fields } from "@cmssy/react";
import Hero from "./Hero";

// Exported on its own, so the component can derive its props from it.
export const heroProps = {
  heading: fields.text({ label: "Heading", defaultValue: "Welcome" }),
  body: fields.richText({ label: "Body" }),
  ctaUrl: fields.link({ label: "CTA URL", defaultValue: "/signup" }),
  showCta: fields.boolean({ label: "Show CTA", defaultValue: true }),
};

export const heroBlock = defineBlock({
  type: "hero",
  label: "Hero",
  component: Hero,
  props: heroProps,
});

Field types: text, textarea, richText, markdown, number, date, datetime, boolean, color, media, link, url, email, select, radio, multiselect, relation, repeater, table, json, form, pageSelector. See Schema & Field Types.


The block component

Blocks receive { content, context, data }. The content is already resolved for the active locale, so read fields directly.

// blocks/hero/Hero.tsx
import type { BlockProps } from "@cmssy/react";
import { heroProps } from "./block";

export default function Hero({ content, context }: BlockProps<typeof heroProps>) {
  // Typed from the schema: heading is a string, showCta a boolean.
  const { heading, showCta, ctaUrl } = content;
  const isPreview = context?.isPreview ?? false;

  return (
    <section>
      <h1>{heading}</h1>
      {showCta && <a href={ctaUrl}>Get started</a>}
    </section>
  );
}

BlockProps<typeof heroProps> makes the schema the only place a field is named. Rename heading and the component stops compiling - instead of quietly rendering nothing, which is what reading content.heading off a Record<string, unknown> would have done. When the block has a loader, pass its return type as the second argument: BlockProps<typeof heroProps, Posts> types the data prop too.

context carries locale ({ current, default, enabled }) and isPreview (true inside the editor), plus forms for any form the block references. auth and workspace are present when your app supplies them through buildBlockContext. Locale-aware UI reads context.locale.enabled. For your own models or records fetch with createCmssyClient; to fetch during SSR add a loader and read its result from data. See Advanced Features and Server loaders.


Register & ship

// cmssy/blocks.ts
import { heroBlock } from "@/blocks/hero/block";
export const blocks = [heroBlock];

Run pnpm dev, open the page editor, and your block appears in the picker. Deploy your Next.js app to ship — there is no separate block build or publish step.


Next Steps

Start building blocks

Set up the SDK, then create and register your first block.