Advanced Block Features
Layout blocks, server vs client components, and fetching data with server loaders.
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 atopacity:0and 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.