Cómo construir un bloque de artículos de blog
Construye en tu propia app Next.js un bloque que obtiene y lista artículos de blog desde Cmssy a través de la API de entrega, con búsqueda y paginación.
Qué vamos a construir
Un bloque de listado de artículos que obtiene y muestra los posts de tu workspace de Cmssy a través de la API de entrega. Cmssy es un CMS headless con editor visual: tu contenido vive en el admin de Cmssy y tus componentes de bloques viven en tu propia app Next.js, que despliegas tú mismo. Funciones:
- Posts obtenidos con
createCmssyClientde@cmssy/react - Imágenes de portada desde un campo
media - Búsqueda en el cliente
- Paginación (cargar más)
- Filtrado por categoría
- Modos de diseño en cuadrícula y lista
Cómo funcionan los bloques en un montaje headless
Un bloque es solo un componente React más un esquema, ambos en tu repo. No ejecutas un CLI ni un paso de publicación. El flujo:
- Define los campos editables del bloque en
blocks/blog-posts/block.tscondefineBlock - Escribe el componente React en
blocks/blog-posts/src/ - Regístralo en
cmssy/blocks.ts - Ejecuta
pnpm devy el bloque aparece en el editor - Despliega tu app Next.js - así es como se publica el bloque
El editor enmarca tu sitio desplegado (o el de desarrollo local) y aprende el esquema de cada bloque a través del puente del SDK, así que no hay nada que subir por separado.
Paso 1: define el bloque
Crea 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,
},
},
});Tipos de campo disponibles: singleLine, multiLine, richText, numeric, date, media, link, select, multiselect, boolean, color y repeater. Aquí el editor solo configura el bloque - los posts en sí vienen de un modelo de contenido que consultas en tiempo de renderizado.
Paso 2: entiende las props del componente
Tu componente recibe { content, context }. content contiene los valores de campo de defineBlock; context describe el entorno de renderizado:
interface BlockContext {
locale: {
current: string; // e.g. "en"
default: string; // workspace default locale
enabled: string[]; // all enabled locales
};
isPreview: boolean; // true inside the editor
}Usa context.locale.current para elegir los valores localizados correctos, y context.isPreview para ajustar el comportamiento dentro del editor (por ejemplo, omitir el scroll infinito mientras se edita).
Paso 3: obtén los posts con la API de entrega
Los posts viven como registros de un modelo de contenido (aquí, un modelo post) en tu workspace. Obténlos con createCmssyClient de @cmssy/react, que habla con la API de entrega mediante 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 };
}Cada registro expone los campos que definiste en el modelo. Para un artículo de blog eso suele ser un título, un extracto, un slug y un campo media cover_image. Léelos directamente del registro que devuelve la API de entrega.
Paso 4: construye el componente
Crea 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>
);
}Paso 5: registra el bloque
Añade el bloque a cmssy/blocks.ts para que el editor y tu renderizador lo conozcan:
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
};Ese es todo el cableado. Sin subidas ni comandos de publicación - el editor lee el esquema desde tu app a través del puente del SDK.
Paso 6: añade paginación
Para cargar más posts, mantén un offset y añade resultados cuando el usuario haga clic en "Cargar más" (o cuando un centinela entre en pantalla):
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>}Dentro del editor puedes atajar esto con context.isPreview para que los editores vean una primera página representativa sin disparar peticiones extra.
Ejecútalo en local
Arranca tu app y el bloque aparece en el editor de inmediato:
pnpm devAbre tu workspace en el admin de Cmssy, suelta el bloque Blog Posts en una página y el editor enmarcará tu sitio local. Configura el encabezado, elige el modelo de posts y la lista se renderiza en vivo desde la API de entrega.
Públicalo
No hay un paso de despliegue separado para los bloques. Cuando despliegas tu app Next.js (en Vercel o donde sea), el nuevo bloque va con ella. El editor apunta a tu URL desplegada para la edición visual. Configura NEXT_PUBLIC_CMSSY_WORKSPACE y un token de entrega en tu entorno de producción y listo.
Configuración del modelo
Antes de que tu bloque tenga algo que listar, crea el modelo de contenido en el admin de Cmssy:
- Ve a Models y crea un modelo
post - Añade campos:
title(Single line)excerpt(Multi line)cover_image(Media)category(Select con opciones)
- Crea registros para cada post y rellena los campos
- Genera un token de entrega para que tu bloque pueda leerlos
Siguientes pasos
- Lee la guía de desarrollo de bloques
- Consulta todos los tipos de campo y opciones de esquema
- Explora la referencia de bloques
- Sigue la guía de instalación para configurar
@cmssy/reacty@cmssy/next