What you need before you start

Node 20+, a package manager (the examples use pnpm, npm works the same), a cmssy workspace and one API token. Create the token on the setup card of your workspace home or under Settings → API Tokens; it starts with cs_. Forty minutes end to end, most of it reading. Deploying (step 7) needs any host that runs Next.js; the walkthrough was verified with a tunnel standing in for the deploy.

Your first block, live

One path, no detours: a fresh Next.js app, the cmssy wiring, your local site framed in the editor, a block you wrote edited in place, then deployed, promoted, published - and a webhook so a publish shows up now, not after the cache expires. Every step below was run end to end on 2026-09-20 with @cmssy/cli 16.11.0 and Next.js 16.3.

20 septembre 2026

The other pages in this section explain each piece on its own. This one walks the whole path in order, with the exact commands and the exact labels you will see. At the end you have a Next.js site that renders your workspace, a block of your own on a published page, and a workspace that tells your site when to refresh.

1. Create the app and wire cmssy

Start from your framework's own generator, then let the cmssy CLI add the wiring. It detects Next.js from package.json. Always call the CLI as @cmssy/cli@latest: a stale global install would otherwise win and fail with Cannot query field "myWorkspaces".

npx create-next-app@latest my-site   # App Router: yes
cd my-site
npx @cmssy/cli@latest init
pnpm install

init writes 16 files and never overwrites without --force:

  • cmssy.config.ts - reads CMSSY_ORG_SLUG, CMSSY_WORKSPACE_SLUG and CMSSY_DRAFT_SECRET straight from process.env, so a missing variable fails at startup with its name instead of somewhere unrelated later. .env.example lists them; link fills the real .env.local in the next step.
  • proxy.ts - the middleware preset: locale prefixes, the verified edit-mode rewrite, and the CSP that lets only the cmssy admin frame your site.
  • cmssy/blocks.ts - the block registry, with an example hero block in blocks/hero/. Next to it: cmssy/editor.tsx (loads the registry lazily on the client), cmssy/editable-layout.tsx (header and footer through the edit bridge) and cmssy/site-providers.tsx (the one place your own providers go).
  • app/[[...path]]/page.tsx and layout.tsx - the public catch-all that renders every cmssy page, plus services/pages.ts it reads from. Your generator's app/page.tsx and app/layout.tsx move to .cmssy-backup/.
  • app/cmssy-edit/[[...path]]/ - the route the editor is rewritten to, with its own root layout; both layouts import globals.css, so keep them in step when you add CSS or metadata.
  • app/api/draft/route.ts and app/api/revalidate/route.ts - draft preview and the publish webhook you will wire in step 8.

init also wires @cmssy/eslint-plugin into eslint.config.mjs and adds @cmssy/next, @cmssy/react, @cmssy/core and the plugin to package.json at its own version - that is why the install comes after it.

2. Connect the app to your workspace

npx @cmssy/cli@latest link --token cs_...

link fetches the organization and workspace slugs and the draft secret with your token, writes the three into .env.local, checks that the workspace answers, that the secret matches and that /api/draft is mounted, and prints the editor deep link for this workspace. If the workspace has no block manifest yet it also pushes one from your registry, so the editor palette knows your blocks before the first deploy. If your token can see several workspaces it asks which one - in a non-interactive shell it stops instead, so pass --workspace <slug>. The output ends with draft-preview links that embed the secret: do not paste them anywhere public.

Prefer link to copying by hand. If you do copy, every value is under Settings → Headless: Organization, Workspace, and Draft preview secret with its Copy button. Regenerate invalidates the old secret immediately - update .env.local when you use it.

3. The environment, in one place

VariableSet byWhat it does
CMSSY_ORG_SLUGlinkOrganization slug. Required.
CMSSY_WORKSPACE_SLUGlinkWorkspace slug. Required.
CMSSY_DRAFT_SECRETlinkServer-only. Gates draft preview and edit mode: the editor sends it, proxy.ts verifies it. Required.
CMSSY_WEBHOOK_SECRETyou, step 8Signing secret of the content.changed webhook that hits /api/revalidate. Without it the route answers 500.
CMSSY_API_TOKENoptionalLets you omit --token on CLI commands. Never needed by the running site.
NEXT_PUBLIC_SITE_URLoptionalYour own public origin, if you build canonical URLs, hreflang or a sitemap like the starter does. cmssy stores canonical content and never your domain.

Keep the same names in your host's environment when you deploy (step 7). Do not add ?? "" fallbacks: an empty slug is a silent 404, a named missing variable is a fix.

4. Run it and frame it in the editor

pnpm dev   # http://localhost:3000

On its own, localhost:3000 renders published content - a fresh workspace shows the starter's Nothing published yet at / page, and that is correct. The live editing happens in the cmssy editor, which frames your running app:

  1. Open the editor link that link printed (or Pages in the dashboard) and open a page - create one if the workspace is empty.
  2. In the editor header click Dev host. Turn on Enable dev host, enter http://localhost:3000 under Local dev host and click Apply. If the page has unsaved changes you are asked to confirm Enter dev preview?.

A blue banner appears: Dev preview - changes save to your isolated dev buffer, not production. The canvas now shows your local app, and the block palette (Add block, or the Blocks tab in the left panel) lists whatever cmssy/blocks.ts exports - just Hero for now. The editor reads the schemas from the framed site itself on every handshake. Three things to know about Dev host:

  • It accepts localhost URLs only (localhost, 127.0.0.1, ::1, *.localhost), http or https. Chrome frames plain http from the https editor; Firefox and Safari may block it, use a tunnel there.
  • It is yours alone. The URL lives in your browser session and the on/off switch is per user. The team keeps using the saved Preview URL (Settings → Headless), which Dev host does not touch.
  • While it is on, edits go to your dev buffer, not to the shared page draft, and Publish is disabled. Step 7 moves the buffer into the real draft. Reloading the editor is fine: the framed site loads the shared draft, and the editor puts your buffered blocks back on the canvas as soon as the site reports in.

5. Write a block

Let the CLI scaffold it; the name must be kebab-case:

npx @cmssy/cli@latest add block feature-card

This creates two files and registers the block in cmssy/blocks.ts for you:

// blocks/feature-card/FeatureCard.tsx
import { fields, type BlockProps } from "@cmssy/react";

export const featureCardProps = {
  heading: fields.text({ label: "Heading", required: true }),
  text: fields.textarea({ label: "Text" }),
};

export default function FeatureCard({
  content,
}: BlockProps<typeof featureCardProps>) {
  return (
    <section>
      <h2>{content.heading}</h2>
      {content.text ? <p>{content.text}</p> : null}
    </section>
  );
}
// blocks/feature-card/block.ts
import { defineBlock } from "@cmssy/react";
import FeatureCard, { featureCardProps } from "./FeatureCard";

export const featureCardBlock = defineBlock({
  type: "feature-card",
  label: "Feature card",
  component: FeatureCard,
  props: featureCardProps,
});

The schema is the only place a field is named: BlockProps<typeof featureCardProps> types content from it, so renaming heading is a compile error rather than an empty card. Give the scaffold some shape before you go back to the editor - it ships unstyled, and an empty <h2> is zero pixels tall, so you would see nothing until you type:

<section className="mx-auto max-w-2xl px-6 py-16">
  <h2 className="text-3xl font-semibold">{content.heading}</h2>
  {content.text ? <p className="mt-4 text-lg">{content.text}</p> : null}
</section>

No restart is needed: hot reload reloads the framed page and the editor re-reads the schemas - Feature Card shows up in the palette within a few seconds. If it does not, reload the editor page once. Everything else about fields, loaders and layout blocks is in Block Development Guide.

6. Put it on the page and edit it live

Back in the editor (Dev host still on): drag Feature Card from the palette onto the canvas, fill Heading and Text in the properties panel and watch the section update in your app as you type. Change the component's markup in your code editor and the canvas follows on hot reload.

No warning appears for the new block: the editor counts a type the framed site declares as known, even though the workspace's block manifest only learns it in step 7. If a yellow notice does list a type, the running site really does not render that block - restore the block in code or remove it from the page.

Click Save page. The toast says Saved to your dev buffer: this is your personal working copy, so a block that does not exist in the deployed site yet cannot break anybody's preview.

7. Ship the block, promote, publish

Three things have to know about feature-card before its content can go public: the deployed site (so visitors can render it), the shared editor preview (which frames the saved Preview URL), and the workspace's block manifest (which Promote to draft validates against). In that order:

  1. Deploy the app (for example vercel, or push to the connected repository) with the same variables from step 3 set in the host's environment.
  2. Point the Preview URL at the deployment - Settings → Headless → Preview URL, or npx @cmssy/cli@latest link --token cs_... --preview-url https://my-site.vercel.app.
  3. Update the block manifest from the code you just deployed: npx @cmssy/cli@latest sync-manifest --token cs_.... It prints exactly what changes (here: adds feature-card) and activates the new manifest. Alternative without the CLI: turn Dev host off once - the editor reads the deployed site's blocks and, because this change only adds a type, activates it on its own; a change that removes or reshapes types is only proposed and waits for review under Settings → Headless → Block manifest.
  4. Back in the editor with Dev host on, click Promote to draft in the blue banner. Your dev buffer becomes the page's real draft. Skipping item 3 gets you a toast naming the missing type - Block type feature-card is not in the workspace block manifest yet - with the same two ways out.
  5. Turn Dev host off (Dev host → switch off; confirm Exit dev preview? if asked). The canvas now frames the deployed site and renders your block there.
  6. Click Publish.

Publishing does not deploy anything - the block already shipped in item 1; publishing only flips the content to public.

8. Make a publish show up now: the revalidate webhook

The catch-all route generated by init caches: export const revalidate = 3600. Without help, a page you publish keeps serving the old copy for up to an hour. cmssy closes that gap by calling your site on every publish - you only have to register the endpoint.

  1. In the dashboard open Settings → Webhooks and click Add endpoint.
  2. URL: https://your-site.com/api/revalidate. Events: content.changed (one event that fires for every page, record, media and settings change; you do not need the granular ones for cache purposes).
  3. Copy the secret now - it is shown once. Set it as CMSSY_WEBHOOK_SECRET in your host's environment and redeploy.

What happens on publish: cmssy signs the payload with HMAC-SHA256 over timestamp.body and sends it with the x-cmssy-signature: t=…,v1=… header. createCmssyRevalidateRoute in app/api/revalidate/route.ts verifies the signature against your secret and expires the cmssy content cache tags, so the next request renders fresh content. Recent deliveries on the same settings page shows each call and its response; Rotate secret is there when you need a new one - the old one stops working immediately, so update the environment in the same move.

To try it before you deploy, expose the dev server with a tunnel (cloudflared tunnel --url http://localhost:3000 or ngrok), register the tunnel URL as the endpoint, put the secret in .env.local and restart pnpm dev (env changes are not hot-reloaded). A publish then shows up as POST /api/revalidate 200 in the terminal and as a success row under Recent deliveries.

9. If something does not line up

  • The block is not in the palette. Is Dev host on and pointing at the port your app runs on? Is the block in the blocks array of cmssy/blocks.ts? Reload the editor page once.
  • The block is in the layers list but the canvas shows nothing. The block is empty or unpadded (an empty heading has no height) or hidden under the yellow notice - type a heading, dismiss the notice, add padding.
  • The canvas shows published content instead of edit mode. The draft secret the editor sent does not match your CMSSY_DRAFT_SECRET - run link again. An unverified edit request deliberately renders the public site.
  • Promote to draft says a block type is not in the workspace block manifest yet. The manifest predates your block. Run sync-manifest from the deployed code, or turn Dev host off once with the Preview URL pointing at a deployment that has the block, then promote again.
  • Publish is greyed out. You are in dev preview. Promote to draft, exit Dev host, publish.
  • Published, but the site shows the old page. Check Recent deliveries under Settings → Webhooks. A 500 means CMSSY_WEBHOOK_SECRET is missing on the host; a 401 means it does not match; no delivery at all means the endpoint is not subscribed to content.changed.
  • link says the workspace has a manifest already. Expected after the first run. sync-manifest replaces it when you want the palette to match a registry you changed without opening the editor.
  • npx @cmssy/cli fails with "Cannot query field myWorkspaces". An old global @cmssy/cli answered instead of the current one. Use npx @cmssy/cli@latest or remove the global install.

Next

  • Schema & field types - every field, repeaters, conditional fields.
  • Server loaders - fetching during SSR without shipping the client the query.
  • Draft preview - the three ways to see unpublished content and why ?cmssyEdit=1 alone does nothing.
  • Webhooks - all events and the signature in detail.
  • CLI - init, link, add block, sync-manifest.

Keep building

Fields, loaders and layout blocks pick up where the first block ends.