Ahora con creación de páginas por IA vía el servidor MCP
Tutorial

Cómo construir un bloque de artículos de blog

Crea un bloque dinámico de listado de artículos con datos de colección, búsqueda, scroll infinito e imágenes de portada. Recorrido completo por el código.

E
Equipo Cmssy
15 min read

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.

Last updated: 15 de marzo de 2026

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 createCmssyClient de @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:

  1. Define los campos editables del bloque en blocks/blog-posts/block.ts con defineBlock
  2. Escribe el componente React en blocks/blog-posts/src/
  3. Regístralo en cmssy/blocks.ts
  4. Ejecuta pnpm dev y el bloque aparece en el editor
  5. 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 dev

Abre 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:

  1. Ve a Models y crea un modelo post
  2. Añade campos:
    • title (Single line)
    • excerpt (Multi line)
    • cover_image (Media)
    • category (Select con opciones)
  3. Crea registros para cada post y rellena los campos
  4. Genera un token de entrega para que tu bloque pueda leerlos

Siguientes pasos

Cómo crear un bloque de posts del blog - Tutorial de Cmssy