Comment créer un bloc d'articles de blog
Construisez dans votre propre application Next.js un bloc qui récupère et liste les articles de blog depuis Cmssy via l'API de diffusion, avec recherche et pagination.
Ce que nous construisons
Un bloc de liste d'articles de blog qui récupère et affiche les posts de votre workspace Cmssy via l'API de diffusion. Cmssy est un CMS headless avec éditeur visuel : votre contenu vit dans l'admin Cmssy, et vos composants de blocs vivent dans votre propre application Next.js que vous déployez vous-même. Fonctionnalités :
- Articles récupérés avec
createCmssyClientde@cmssy/react - Images de couverture depuis un champ
media - Recherche côté client
- Pagination (charger plus)
- Filtrage par catégorie
- Modes de mise en page grille et liste
Comment fonctionnent les blocs en headless
Un bloc, c'est juste un composant React plus un schéma, tous deux dans votre repo. Pas de CLI ni d'étape de publication. Le flux :
- Définissez les champs éditables du bloc dans
blocks/blog-posts/block.tsavecdefineBlock - Écrivez le composant React dans
blocks/blog-posts/src/ - Enregistrez-le dans
cmssy/blocks.ts - Lancez
pnpm devet le bloc apparaît dans l'éditeur - Déployez votre application Next.js - c'est ainsi que le bloc est livré
L'éditeur encadre votre site déployé (ou en dev local) et apprend le schéma de chaque bloc via le pont SDK, il n'y a donc rien à uploader séparément.
Étape 1 : définissez le bloc
Créez 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,
},
},
});Types de champs disponibles : singleLine, multiLine, richText, numeric, date, media, link, select, multiselect, boolean, color et repeater. Ici, l'éditeur ne configure que le bloc - les articles eux-mêmes viennent d'un modèle de contenu que vous récupérez au moment du rendu.
Étape 2 : comprenez les props du composant
Votre composant reçoit { content, context }. content contient les valeurs des champs de defineBlock ; context décrit l'environnement de rendu :
interface BlockContext {
locale: {
current: string; // e.g. "en"
default: string; // workspace default locale
enabled: string[]; // all enabled locales
};
isPreview: boolean; // true inside the editor
}Utilisez context.locale.current pour choisir les bonnes valeurs localisées, et context.isPreview pour ajuster le comportement dans l'éditeur (par exemple, désactiver le défilement infini pendant l'édition).
Étape 3 : récupérez les articles avec l'API de diffusion
Les articles vivent comme des enregistrements d'un modèle de contenu (ici, un modèle post) dans votre workspace. Récupérez-les avec createCmssyClient de @cmssy/react, qui communique avec l'API de diffusion via GraphQL :
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 };
}Chaque enregistrement expose les champs que vous avez définis sur le modèle. Pour un article de blog, c'est typiquement un titre, un extrait, un slug et un champ media cover_image. Lisez-les directement sur l'enregistrement renvoyé par l'API de diffusion.
Étape 4 : construisez le composant
Créez 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>
);
}Étape 5 : enregistrez le bloc
Ajoutez le bloc à cmssy/blocks.ts pour que l'éditeur et votre moteur de rendu le connaissent tous les deux :
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
};C'est tout le câblage. Pas d'upload, pas de commande de publication - l'éditeur lit le schéma depuis votre application via le pont SDK.
Étape 6 : ajoutez la pagination
Pour charger plus d'articles, gardez un offset et ajoutez les résultats quand l'utilisateur clique sur "Charger plus" (ou quand une sentinelle entre dans le viewport) :
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>}Dans l'éditeur, vous pouvez court-circuiter cela avec context.isPreview pour que les éditeurs voient une première page représentative sans déclencher de requêtes supplémentaires.
Lancez-le en local
Démarrez votre application et le bloc apparaît immédiatement dans l'éditeur :
pnpm devOuvrez votre workspace dans l'admin Cmssy, déposez le bloc Blog Posts sur une page, et l'éditeur encadre votre site local. Configurez le titre, choisissez le modèle d'articles, et la liste s'affiche en direct depuis l'API de diffusion.
Mettez-le en production
Il n'y a pas d'étape de déploiement séparée pour les blocs. Quand vous déployez votre application Next.js (sur Vercel ou ailleurs), le nouveau bloc part avec elle. L'éditeur pointe vers votre URL déployée pour l'édition visuelle. Définissez NEXT_PUBLIC_CMSSY_WORKSPACE et un token de diffusion dans votre environnement de production, et c'est terminé.
Configuration du modèle
Avant que votre bloc ait quelque chose à lister, créez le modèle de contenu dans l'admin Cmssy :
- Allez dans Models et créez un modèle
post - Ajoutez des champs :
title(Single line)excerpt(Multi line)cover_image(Media)category(Select avec options)
- Créez des enregistrements pour chaque article et remplissez les champs
- Générez un token de diffusion pour que votre bloc puisse les lire
Prochaines étapes
- Lisez le guide de développement de blocs
- Consultez tous les types de champs et options de schéma
- Parcourez la référence des blocs
- Suivez le guide d'installation pour configurer
@cmssy/reactet@cmssy/next