Saltar a contenido

Arquitectura — Payload CMS

Visión general

Este repositorio es un fork del website template oficial de Payload CMS, reconvertido en CMS headless para la plataforma de e-commerce. Corre como una única aplicación Next.js 15 que contiene tanto el panel de administración de Payload como una API REST/GraphQL, y persiste en PostgreSQL.

Su rol dentro de la plataforma es ser la fuente de verdad del contenido editorial y comercial (banners, sliders, menú de navegación, páginas, posts, medios). No administra productos ni stock: eso vive en el backend Medusa, y el descubrimiento de productos se resuelve con Algolia. El bloque productGrid de este CMS guarda únicamente un descriptor de filtro para Algolia — el CMS nunca consulta productos.

El sitio Next.js (frontend) incluido es en gran medida código heredado del template. Los bloques comerciales (hero, heroSlider, productGrid) no tienen componente React a propósito: los consume y renderiza el storefront externo (ver las entradas comentadas en src/blocks/RenderBlocks.tsx).

Componentes

Componente Responsabilidad Tecnología
Panel de administración Alta y edición de contenido por parte de los editores, en /admin Payload 3.61.1 (src/app/(payload)/)
API de contenido Expone colecciones y globales al storefront en /api y /api/graphql Payload REST + GraphQL
Frontend local Sitio Next.js heredado del template (páginas, posts, live preview) Next.js 15.4.4 / React 19 (src/app/(frontend)/)
Capa de medios Sube, entrega y borra imágenes contra Cloudflare Images mediante hooks de colección src/collections/Media/hooks.ts + src/utilities/cloudflare-images.ts
Esquema de contenido Colecciones (pages, posts, media, categories, users, banners, sliders) y globales (header, footer, main-navigation) — detalle en Inventario de entidades src/payload.config.ts
Migraciones Esquema versionado que se aplica en producción src/migrations/ (Drizzle vía @payloadcms/db-postgres)

Colecciones y globales que lee el storefront — banners, sliders, media, main-navigation, header, footer — tienen acceso público de lectura (anyone / () => true). Esa configuración es parte del contrato con el storefront y no debe restringirse sin coordinar el cambio.

Dependencias externas

Dependencia Uso ¿Qué pasa si no está?
PostgreSQL (DATABASE_URI) Toda la persistencia de contenido El sistema no arranca. También se necesita en tiempo de build: el docker build falla si la base no es alcanzable
Cloudflare Images (CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_IMAGES_TOKEN, CLOUDFLARE_ACCOUNT_HASH) Almacenamiento y entrega de todas las imágenes Degradación controlada: sin credenciales, la subida se saltea con un console.warn y no se generan URLs; el resto del CMS sigue funcionando. Las imágenes ya subidas dejan de entregarse si el servicio se cae
Storefront externo Consumidor de la API; su origen debe estar en el array cors de src/payload.config.ts El CMS sigue funcionando; el storefront deja de recibir contenido (o lo bloquea CORS si el origen no está listado)
Algolia El bloque productGrid guarda un descriptor de filtro (filterType + filterValue, sortBy, itemsPerPage) que resuelve el storefront El CMS no se ve afectado — nunca llama a Algolia. Las grillas de producto del storefront quedan vacías
imagedelivery.net CDN de entrega de las variantes de imagen Las imágenes no cargan en admin ni en el storefront

Variables de entorno adicionales: NEXT_PUBLIC_SERVER_URL (sin barra final), PAYLOAD_SECRET (firma de JWT), CRON_SECRET (autoriza el endpoint de jobs vía Bearer) y PREVIEW_SECRET (valida el preview de borradores).

Diagrama

flowchart LR
    E[Editor] -->|HTTPS /admin| P[Payload CMS<br/>Next.js 15]
    S[Storefront e-commerce] -->|REST / GraphQL| P
    P -->|SQL| DB[(PostgreSQL)]
    P -->|API v1 upload/delete| CF[Cloudflare Images]
    S -->|GET variante| CDN[imagedelivery.net]
    CF -.entrega.-> CDN
    S -->|búsqueda de productos| AL[Algolia]

Flujo principal

Alta de una imagen y su consumo por el storefront — el camino que explica por qué los medios son el punto más delicado del sistema:

  1. El editor sube un archivo desde el panel de administración a la colección media. La colección tiene upload.disableLocalStorage: true, así que Payload no escribe nada en disco.
  2. El hook beforeChange (solo en create) toma el buffer y lo sube a la API de Cloudflare Images. Persiste en la base únicamente cfImageId, más filename, filesize y mimeType — estos tres son obligatorios para que el admin reconozca el documento como imagen.
  3. En cada lectura, el hook afterRead sintetiza a partir de cfImageId los campos url, thumbnailURL, baseDeliveryUrl y todo el grupo sizes. Son campos virtuales: nunca se persisten. Cualquier consumidor que lea la tabla directamente por SQL no verá ninguno.
  4. El storefront pide el documento por GET /api/media/<id> (o embebido en un banner/slider) y recibe las URLs ya resueltas contra imagedelivery.net.
  5. Al borrar, el hook beforeDelete elimina la imagen remota pero traga los errores: el borrado en Payload siempre procede, aunque Cloudflare falle.

Los nombres de IMAGE_VARIANTS (thumbnail, square, small, medium, large, xlarge, og) deben existir textualmente como variantes en el dashboard de Cloudflare o las URLs generadas devuelven 404. Como salida de emergencia, USE_PUBLIC_VARIANT = true en src/utilities/cloudflare-images.ts hace que todas las variantes caigan a public.