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:
- El editor sube un archivo desde el panel de administración a la colección
media. La colección tieneupload.disableLocalStorage: true, así que Payload no escribe nada en disco. - El hook
beforeChange(solo encreate) toma el buffer y lo sube a la API de Cloudflare Images. Persiste en la base únicamentecfImageId, másfilename,filesizeymimeType— estos tres son obligatorios para que el admin reconozca el documento como imagen. - En cada lectura, el hook
afterReadsintetiza a partir decfImageIdlos camposurl,thumbnailURL,baseDeliveryUrly todo el gruposizes. Son campos virtuales: nunca se persisten. Cualquier consumidor que lea la tabla directamente por SQL no verá ninguno. - El storefront pide el documento por
GET /api/media/<id>(o embebido en un banner/slider) y recibe las URLs ya resueltas contraimagedelivery.net. - Al borrar, el hook
beforeDeleteelimina 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.