Erweiterte Block-Funktionen
Layout-Blöcke, Tailwind-Styling, Server- vs. Client-Komponenten und Datenabruf mit Server-Loadern.
Layout-Blöcke
Header, Footer und andere gemeinsame Bereiche sind Layout-Blöcke — identisch mit Seiten-Blöcken, aber mit layoutPositions markiert und von CmssyServerLayout pro Position gerendert.
// blocks/header/block.ts
import type { ComponentType } from "react";
import { defineBlock, fields } from "@cmssy/react";
import Component from "./src";
export const headerBlock = defineBlock({
type: "header",
label: "Header",
layoutPositions: ["header"],
component: Component as unknown as ComponentType<{ content: Record<string, unknown> }>,
props: {
logo: fields.media({ label: "Logo" }),
links: fields.repeater({
label: "Navigation",
itemSchema: {
label: fields.singleLine({ label: "Label" }),
url: fields.link({ label: "URL" }),
},
}),
},
});
Rendere die Position in app/layout.tsx mit CmssyServerLayout.
Styling mit Tailwind v4
Blöcke werden mit Tailwind gestylt. Importiere dein globales Stylesheet einmal und nutze dann Utility-Klassen in deinen Komponenten.
/* styles/main.css */
@import "tailwindcss";
@theme {
--color-background: oklch(1 0 0);
--color-foreground: oklch(0.145 0 0);
--color-primary: oklch(0.205 0 0);
}
Server- & Client-Komponenten
Blöcke sind Standard-Next.js-Komponenten. Standardmäßig rendern sie als Server-Komponenten — null Client-JS. Füge "use client" nur dann oben in einer Komponente hinzu, wenn sie Hooks, Event-Handler, Browser-APIs oder Client-Animationen braucht.
"use client";
import { useState } from "react";
export default function StatsCounter({ content }: { content: Record<string, unknown> }) {
const [n, setN] = useState(0);
// ...
}
Scroll-/Eingangsanimationen brauchen eine Client-Komponente. Wenn ein Block Reveal-on-Scroll nutzt (framer-motion
whileInView,initial={{ opacity: 0 }}, IntersectionObserver), muss er eine Client-Komponente sein ("use client"), damit die Animation läuft — sonst bleibt das Server-HTML beiopacity:0und der Inhalt blendet auf der veröffentlichten Seite nie ein (im Editor sieht es gut aus, denn der rendert client-seitig).
Daten abrufen: Server-Loader
Die meisten Blöcke sollten auf dem Server, während des SSR, per loader laden. Der Loader läuft in CmssyServerPage und übergibt sein Ergebnis als data-Prop an die Komponente — so landet der Inhalt im server-gerenderten HTML (crawlbar, kein Lade-Flackern) und server-only-Abhängigkeiten erreichen nie das 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 });
},
});
Der Loader läuft nicht im Editor — dort erhält die Komponente data: undefined, also rendere immer einen sinnvollen Fallback. Der Rückgabewert überquert die Server→Client-Grenze und muss daher RSC-serialisierbar sein: einfache Objekte, Arrays und Primitive — keine Funktionen oder Klasseninstanzen.
Halte server-only- oder schwere Abhängigkeiten hinter einem dynamischen import() im Loader (wie oben) und sichere gemeinsame Server-Helper mit typeof window !== "undefined" ab, damit sie laut fehlschlagen, falls sie je für den Browser gebundelt werden.
Die Delivery-API aufrufen
Nutze createCmssyClient(...).queryScoped(...) aus @cmssy/react. queryScoped injiziert workspaceId automatisch: Wenn deine Query $workspaceId deklariert und du sie nicht übergibst, löst das SDK sie aus deinem workspaceSlug auf und fügt sowohl die Variable als auch den x-workspace-id-Header hinzu.
// 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-seitige Daten & der Block-Context
Für Interaktivität nach dem First Paint (Suche, Pagination, Filterung) initialisiere den Client-State aus dem SSR-data und lade den Rest client-seitig mit demselben createCmssyClient. Der Block-context trägt außerdem Daten, die du oft ohne Fetch brauchst:
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
Siehe Mitglieder-Authentifizierung für context.auth und den sicheren Auth-Flow.