Now with AI-powered page building via MCP Server

Advanced Block Features

Layout blocks, server vs client components, and fetching data with server loaders.

Last updated: June 29, 2026

Layout blocks

Header, footer and other shared regions are layout blocks — the same as page blocks, but tagged with layoutPositions and rendered by CmssyServerLayout per position.

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

export const headerBlock = defineBlock({
  type: "header",
  label: "Header",
  layoutPositions: ["header"],
  component: Header,
  props: {
    logo: fields.media({ label: "Logo" }),
    links: fields.repeater({
      label: "Navigation",
      itemSchema: {
        label: fields.text({ label: "Label" }),
        url: fields.link({ label: "URL" }),
      },
    }),
  },
});

Render the position in app/layout.tsx with CmssyServerLayout.


Server & client components

Blocks are standard Next.js components. By default they render as server components — zero client JS. Add "use client" at the top of a component only when it needs hooks, event handlers, browser APIs or client animations.

"use client";
import { useState } from "react";

export default function StatsCounter({ content }: { content: Record<string, unknown> }) {
  const [n, setN] = useState(0);
  // ...
}

Scroll / entrance animations need a client component. If a block uses reveal-on-scroll (framer-motion whileInView, initial={{ opacity: 0 }}, IntersectionObserver), it must be a client component ("use client") so the animation runs — otherwise the server HTML stays at opacity:0 and the content never fades in on the published site (it looks fine in the editor, which renders client-side).


Fetching data: server loaders

Most blocks should fetch on the server, during SSR, via a loader. The loader runs in CmssyServerPage and passes its result to the component as the data prop — so the content lands in the server-rendered HTML (crawlable, no loading flash) and server-only dependencies never reach the client bundle.

import { defineBlock } from "@cmssy/react";

export const blogPostsBlock = defineBlock({
  type: "blog-posts",
  component: BlogPosts, // receives { content, context, data }
  loader: async ({ content, context }) => {
    // runs on the server only; returns RSC-serializable data
    const { loadPosts } = await import("./load-posts");
    return loadPosts({ limit: Number(content.postsPerPage) || 9 });
  },
});

The loader does not run in the editor — there the component receives data: undefined, so always render a sensible fallback. The return value crosses the server→client boundary, so it must be RSC-serializable: plain objects, arrays and primitives — no functions or class instances.

Keep server-only or heavy dependencies behind a dynamic import() inside the loader (as above), and guard shared server helpers with typeof window !== "undefined" so they fail loudly if ever bundled for the browser.

Calling the delivery API

Use createCmssyClient(...).queryScoped(...) from @cmssy/react. queryScoped auto-injects workspaceId: when your query declares $workspaceId and you don't pass it, the SDK resolves it from your workspaceSlug and adds both the variable and the x-workspace-id header.

// load-posts.ts — a server-only helper, imported via dynamic import()
import { createCmssyClient } from "@cmssy/react";
import { cmssy } from "@/cmssy.config";

const client = createCmssyClient(cmssy);

export async function loadPosts(vars: { limit: number }) {
  if (typeof window !== "undefined") throw new Error("loadPosts is server-only");
  return client.queryScoped(PUBLIC_PAGES_QUERY, vars);
}

Client-side data & the block context

For interactivity after first paint (search, pagination, filtering), seed client state from the SSR data and fetch the rest client-side with the same createCmssyClient. The block context also carries data you often need without a fetch:

context.locale;     // { current, default, enabled }
context.isPreview;  // true inside the editor
context.forms;      // resolved form definitions (when present)
context.auth;       // { isAuthenticated, member } when config.auth is set
context.workspace;  // { id, slug } when resolved

See Member Authentication for context.auth and the secure auth flow.


Next Steps