So baust du einen Blog-Posts-Block
Baue in deiner eigenen Next.js-App einen Block, der Blogposts über die Delivery-API aus Cmssy holt und auflistet - mit Suche und Pagination.
Was wir bauen
Ein Blog-Listing-Block, der Posts aus deinem Cmssy-Workspace über die Delivery-API holt und anzeigt. Cmssy ist ein Headless CMS mit visuellem Editor: Deine Inhalte leben im Cmssy-Admin, deine Block-Komponenten in deiner eigenen Next.js-App, die du selbst deployst. Features:
- Posts geladen mit
createCmssyClientaus@cmssy/react - Cover-Bilder aus einem
media-Feld - Clientseitige Suche
- Pagination (mehr laden)
- Kategorie-Filter
- Grid- und Listen-Layout
Wie Blöcke in einem Headless-Setup funktionieren
Ein Block ist einfach eine React-Komponente plus ein Schema, beide in deinem Repo. Du führst kein CLI und keinen Publish-Schritt aus. Der Ablauf:
- Definiere die editierbaren Felder des Blocks in
blocks/blog-posts/block.tsmitdefineBlock - Schreibe die React-Komponente in
blocks/blog-posts/src/ - Registriere sie in
cmssy/blocks.ts - Starte
pnpm devund der Block erscheint im Editor - Deploye deine Next.js-App - so wird der Block ausgeliefert
Der Editor rahmt deine deployte (oder lokale Dev-)Site ein und lernt das Schema jedes Blocks über die SDK-Bridge - es gibt also nichts separat hochzuladen.
Schritt 1: Definiere den Block
Erstelle blocks/blog-posts/block.ts:
import { defineBlock } from "@cmssy/react";
export const blogPostsBlock = defineBlock({
name: "Blog Posts",
description: "Dynamic blog post listing with search and pagination",
category: "content",
fields: {
heading: {
type: "singleLine",
label: "Heading",
defaultValue: "Latest Posts",
},
description: {
type: "multiLine",
label: "Description",
},
postModel: {
type: "singleLine",
label: "Post model",
helpText: "API key of the model holding your posts (e.g. \"post\")",
defaultValue: "post",
},
postsPerPage: {
type: "numeric",
label: "Posts per page",
defaultValue: 9,
},
layout: {
type: "select",
label: "Layout",
options: [
{ label: "Grid", value: "grid" },
{ label: "List", value: "list" },
],
defaultValue: "grid",
},
showSearch: {
type: "boolean",
label: "Show Search",
defaultValue: true,
},
},
});Verfügbare Feldtypen: singleLine, multiLine, richText, numeric, date, media, link, select, multiselect, boolean, color und repeater. Hier konfiguriert der Editor nur den Block selbst - die Posts kommen aus einem Content-Model, das du zur Renderzeit abrufst.
Schritt 2: Verstehe die Komponenten-Props
Deine Komponente erhält { content, context }. content hält die Feldwerte aus defineBlock; context beschreibt die Rendering-Umgebung:
interface BlockContext {
locale: {
current: string; // e.g. "en"
default: string; // workspace default locale
enabled: string[]; // all enabled locales
};
isPreview: boolean; // true inside the editor
}Nutze context.locale.current, um die richtigen lokalisierten Werte zu wählen, und context.isPreview, um das Verhalten im Editor anzupassen (zum Beispiel Infinite Scroll beim Bearbeiten überspringen).
Schritt 3: Lade Posts über die Delivery-API
Posts leben als Records eines Content-Models (hier ein post-Model) in deinem Workspace. Lade sie mit createCmssyClient aus @cmssy/react, das über GraphQL mit der Delivery-API spricht:
import { createCmssyClient } from "@cmssy/react";
const cmssy = createCmssyClient({
workspace: process.env.NEXT_PUBLIC_CMSSY_WORKSPACE!,
token: process.env.NEXT_PUBLIC_CMSSY_DELIVERY_TOKEN!,
});
async function fetchPosts(opts: {
model: string;
locale: string;
search?: string;
limit: number;
offset?: number;
}) {
const { items, total, hasMore } = await cmssy.records.list({
model: opts.model,
locale: opts.locale,
search: opts.search,
limit: opts.limit,
offset: opts.offset ?? 0,
});
return { items, total, hasMore };
}Jeder Record stellt die Felder bereit, die du auf dem Model definiert hast. Bei einem Blogpost sind das typischerweise Titel, Excerpt, Slug und ein cover_image-Media-Feld. Lies sie direkt vom Record, den die Delivery-API zurückgibt.
Schritt 4: Baue die Komponente
Erstelle blocks/blog-posts/src/blog-posts.tsx:
"use client";
import { createCmssyClient } from "@cmssy/react";
import { useEffect, useState } from "react";
import type { BlogPostsContent } from "../block";
interface BlockContext {
locale: { current: string; default: string; enabled: string[] };
isPreview: boolean;
}
interface Props {
content: BlogPostsContent;
context: BlockContext;
}
interface Post {
id: string;
slug: string;
title: string;
excerpt?: string;
cover_image?: string | null;
}
const cmssy = createCmssyClient({
workspace: process.env.NEXT_PUBLIC_CMSSY_WORKSPACE!,
token: process.env.NEXT_PUBLIC_CMSSY_DELIVERY_TOKEN!,
});
export function BlogPosts({ content, context }: Props) {
const { layout = "grid", showSearch = true, postModel = "post", postsPerPage } = content;
const locale = context.locale.current;
const limit = Number(postsPerPage) || 9;
const [items, setItems] = useState<Post[]>([]);
const [hasMore, setHasMore] = useState(false);
const [search, setSearch] = useState("");
useEffect(() => {
let active = true;
cmssy.records
.list({ model: postModel, locale, search: search || undefined, limit })
.then((res) => {
if (!active) return;
setItems(res.items as Post[]);
setHasMore(res.hasMore);
});
return () => {
active = false;
};
}, [postModel, locale, search, limit]);
return (
<section className="py-24">
<div className="max-w-6xl mx-auto px-6">
{showSearch && (
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search posts..."
className="w-full sm:w-80 mb-8 px-4 py-2.5 border rounded-lg"
/>
)}
<div className={`grid grid-cols-1 gap-8 ${
layout === "grid" ? "md:grid-cols-2 lg:grid-cols-3" : ""
}`}>
{items.map((post) => (
<a key={post.id} href={`/blog/${post.slug}`}
className="group flex flex-col rounded-2xl overflow-hidden
border hover:shadow-lg transition-shadow">
{post.cover_image ? (
<div className="aspect-video overflow-hidden">
<img src={post.cover_image} alt={post.title} loading="lazy"
className="w-full h-full object-cover
group-hover:scale-105 transition-transform" />
</div>
) : (
<div className="aspect-video bg-muted" />
)}
<div className="p-5">
<h3 className="text-lg font-semibold">{post.title}</h3>
{post.excerpt && (
<p className="text-sm text-muted-foreground mt-2 line-clamp-3">
{post.excerpt}
</p>
)}
</div>
</a>
))}
</div>
</div>
</section>
);
}Schritt 5: Registriere den Block
Füge den Block zu cmssy/blocks.ts hinzu, damit Editor und Renderer ihn kennen:
import { blogPostsBlock } from "../blocks/blog-posts/block";
import { BlogPosts } from "../blocks/blog-posts/src/blog-posts";
export const blocks = {
"blog-posts": {
...blogPostsBlock,
component: BlogPosts,
},
// ...your other blocks
};Das ist die ganze Verkabelung. Kein Upload, kein Publish-Befehl - der Editor liest das Schema über die SDK-Bridge aus deiner App.
Schritt 6: Füge Pagination hinzu
Um mehr Posts zu laden, halte einen offset und hänge Ergebnisse an, wenn der Nutzer auf "Load more" klickt (oder wenn ein Sentinel in den Viewport scrollt):
const [offset, setOffset] = useState(0);
async function loadMore() {
const next = offset + limit;
const res = await cmssy.records.list({
model: postModel,
locale,
search: search || undefined,
limit,
offset: next,
});
setItems((prev) => [...prev, ...(res.items as Post[])]);
setHasMore(res.hasMore);
setOffset(next);
}
// Render under the grid:
// {hasMore && <button onClick={loadMore}>Load more</button>}Im Editor kannst du das mit context.isPreview abkürzen, sodass Redakteure eine repräsentative erste Seite sehen, ohne zusätzliche Fetches auszulösen.
Lokal ausführen
Starte deine App und der Block erscheint sofort im Editor:
pnpm devÖffne deinen Workspace im Cmssy-Admin, ziehe den Blog-Posts-Block auf eine Seite, und der Editor rahmt deine lokale Site ein. Konfiguriere die Überschrift, wähle das Post-Model, und die Liste rendert live aus der Delivery-API.
Ausliefern
Es gibt keinen separaten Block-Deploy-Schritt. Wenn du deine Next.js-App deployst (zu Vercel oder anderswo), wird der neue Block mitgeliefert. Der Editor zeigt für das visuelle Editing auf deine deployte URL. Setze NEXT_PUBLIC_CMSSY_WORKSPACE und einen Delivery-Token in deiner Produktionsumgebung - fertig.
Model-Setup
Bevor dein Block etwas zum Auflisten hat, erstelle das Content-Model im Cmssy-Admin:
- Gehe zu Models und erstelle ein
post-Model - Füge Felder hinzu:
title(Single line)excerpt(Multi line)cover_image(Media)category(Select mit Optionen)
- Erstelle Records für jeden Post und fülle die Felder aus
- Generiere einen Delivery-Token, damit dein Block sie lesen kann
Nächste Schritte
- Lies den Block-Development-Guide
- Sieh dir alle Feldtypen und Schema-Optionen an
- Stöbere in der Block-Referenz
- Folge dem Installations-Guide, um
@cmssy/reactund@cmssy/nexteinzurichten