Saltar a contenido

Relevamiento del desarrollo propio

Foto del estado del repositorio al 19 de agosto de 2026 (rama develop, commit 9bb294a): qué se construyó encima del website template de Payload, cuánto del repo es propio y qué quedó a medias.

Todas las cifras salen de medir el repositorio con git y wc, no de estimar. El detalle campo por campo de las entidades está en Inventario de entidades; acá interesa el estado, no el modelo.

Líneas propias 1.953
Archivos propios 14
Colecciones 11 (7 configuradas + 4 de plugins)
Globales 3
Bloques en el layout de páginas 8
Commits 10
Payload 3.61.1

Panorama

El repositorio arranca como un fork del website template oficial de Payload —un sitio web completo con blog, SEO, formularios y buscador— y se reconvierte en otra cosa: un CMS headless que solo modela y sirve contenido, mientras el storefront de e-commerce lo renderiza del otro lado de la API.

Todo el desarrollo propio son 1.953 líneas en 14 archivos, sobre unas 29.600 líneas de template (184 archivos en src/, sin contar payload-types.ts). Es una proporción chica a propósito: casi todo el trabajo es configuración de esquema —definir colecciones, campos y bloques— y no lógica de aplicación. La única excepción es la capa de medios, que sí es código.

Se escribió entre el 11 de noviembre y el 4 de diciembre de 2025, en 7 commits por una sola persona. Los 3 commits restantes son la documentación de agosto de 2026.

Nada de lo comercial se renderiza en este repo

Los tres bloques comerciales —hero, heroSlider, productGrid— no tienen componente React acá: existen como configuración y tipos, y los dibuja el storefront. Es una decisión registrada en ADR-0001, no un pendiente.

Balance: qué es propio y qué es andamiaje

El dato que más ordena la lectura del repo: la mayor parte de los archivos son del template y nadie los tocó. De los 184 archivos de src/, solo 24 se modificaron después del commit inicial. Saber cuál es cuál evita mantener código que no cumple ninguna función acá.

Desarrollado acá — 14 archivos, 1.953 líneas

Archivo Qué es Líneas
src/collections/Sliders.ts Colección Sliders 363
src/fields/ctaButton.ts Campo reutilizable de botón CTA 252
src/collections/Media.ts Colección Media, reescrita para Cloudflare 227
src/utilities/cloudflare-images.ts Cliente de la API de Cloudflare Images 223
src/fields/linkConfig.ts Campo reutilizable de enlace 176
src/MainNavigation/config.ts Global Menu de Navegación 164
src/collections/Media/hooks.ts Hooks del ciclo de vida de medios 163
src/ProductGrid.ts Bloque Cuadrícula de Productos 135
src/collections/Sliders/hooks/filterSlides.ts Hook de filtrado de slides 71
src/collections/Banners.ts Colección Banners 67
src/blocks/Hero/Hero.ts Bloque Banner 64
src/blocks/HeroSlider/HeroSlider.ts Bloque Slider 19
src/collections/Sliders/SlideRowLabel.tsx Etiqueta de fila de slide 16
src/MainNavigation/RowLabel.tsx Etiqueta de fila del menú 13

No se cuentan acá dos archivos generados que sí están versionados: src/payload-types.ts (2.212 líneas) y la migración inicial 20251204_115854.ts (1.525 líneas).

Del template, sin tocar — unos 160 archivos

  • Sitio web público (src/app/(frontend)/)
  • Blog: colecciones posts, categories, autores poblados
  • Los 5 bloques que sí tienen componente React
  • Globales header y footer
  • Buscador, SEO, formularios y redirects (6 plugins)
  • Live preview y sistema de borradores
  • Seed de contenido de ejemplo
  • Los 2 tests del scaffold

El sitio público heredado no es sobrante puro: es lo que hace funcionar el live preview de páginas y posts para los editores. Pero todo lo demás de esa lista es superficie que hay que mantener actualizada sin que nadie la consuma.

Lo que administra el editor

Tres entidades propias concentran todo el contenido comercial, más un bloque que describe consultas de productos. Las tres comparten el mismo patrón: el editor elige un destino —filtro de productos, página del CMS o URL libre— y el storefront lo resuelve.

Entidad Qué resuelve Tamaño Lectura
banners Piezas gráficas reutilizables con enlace y botón opcional. El bloque hero no define contenido: apunta a un banner, así que la misma pieza se usa en varias páginas y se edita en un solo lugar 5 campos vigentes, 67 líneas pública
sliders Carruseles completos. Cada uno tiene una key única con la que el storefront lo pide, y adentro slides con imagen desktop y mobile, duración, enlace, botón y ventana de visibilidad propia 4 campos + 10 por slide, 363 líneas pública
main-navigation La barra de menú del storefront. Global: hay uno solo y siempre existe. No confundir con header/footer, que son del template 10 campos por ítem, 164 líneas pública
productGrid Bloque que guarda un descriptor de consulta —tipo de filtro, valor, cantidad por página, orden— que el storefront traduce a Algolia. El CMS nunca consulta productos 5 campos vigentes, 135 líneas —

La ventana de visibilidad por slide es lo que permite programar una campaña sin tocar el slider: se cargan los slides con sus fechas y se publican solos — salvo por el pendiente #1 de más abajo.

Los medios no viven en este servidor

La única parte del proyecto que es código de verdad y no configuración: 386 líneas (223 del cliente HTTP + 163 de hooks) que sacan el almacenamiento de imágenes del contenedor y lo mueven a Cloudflare Images.

La colección media declara disableLocalStorage y delega todo el ciclo de vida a tres hooks. Lo que se guarda en la base de datos es sorprendentemente poco: un identificador de Cloudflare y tres campos de metadata.

flowchart LR
    E[Editor<br/>/admin] -->|sube| P[Payload CMS<br/>hook beforeChange]
    P -->|API v1| CF[Cloudflare Images<br/>7 variantes]
    P -.persiste solo<br/>cfImageId + metadata.-> DB[(PostgreSQL)]
    S[Storefront] -->|REST / GraphQL| P
    P -.hook afterRead<br/>sintetiza las URLs.-> S
    S -->|GET variante| CDN[imagedelivery.net]
    CF -.entrega.-> CDN

Los campos url, thumbnailURL, baseDeliveryUrl y todo el grupo sizes son virtuales: se calculan en cada lectura y nunca se guardan. La consecuencia práctica es que quien lea la tabla media por SQL no va a encontrar ninguna URL, solo el cfImageId. Todo acceso a medios tiene que pasar por la API.

Al borrar, el tercer hook elimina la imagen remota pero se traga los errores a propósito: el borrado en el CMS siempre procede, aunque Cloudflare falle. El costo es que un borrado fallido deja la imagen huérfana allá, en silencio.

Ver ADR-0002 y la guía de configuración.

Esquema: once colecciones, tres globales, ocho bloques

Las entidades de lectura pública son el contrato con el storefront: restringirlas lo rompe.

Entidad Tipo Lectura Para qué Origen
banners Colección pública Piezas gráficas con enlace y botón Propio
sliders Colección pública Carruseles con slides programables Propio
main-navigation Global pública Barra de menú del storefront Propio
media Colección pública Todas las imágenes, en Cloudflare Reescrito
pages Colección si publicada Páginas armadas con bloques Template
posts Colección si publicada Entradas de blog Template
categories Colección pública Categorías anidables de posts Template
users Colección autenticados Usuarios del panel Template
header · footer Globales pública Navegación del sitio heredado Template
forms · form-submissions Colecciones — Formularios y sus envíos Plugin
redirects · search Colecciones — Redirecciones e índice de búsqueda Plugin

De los ocho bloques disponibles al armar una página, tres son propios y no se renderizan acá:

Bloque Nombre en el admin ¿Componente React acá? Origen
hero Banner No — lo renderiza el storefront Propio
heroSlider Slider No — lo renderiza el storefront Propio
productGrid Cuadrícula de Productos No — lo renderiza el storefront Propio
cta · content · mediaBlock · archive · formBlock Los cinco del template Sí Template

Lo que quedó a medias

Todo lo que sigue sale de leer el código, no de incidentes reportados. Están ordenados por cuánto cuesta que exploten.

1. El filtrado de slides está escrito pero no conectado

Existe un hook filterActiveSlides de 71 líneas que descarta en las lecturas los slides desactivados o fuera de su ventana de fechas. La colección no lo importa. Hoy la API devuelve todos los slides, y quien respeta enabled, publishFrom y publishUntil es el storefront.

Si el storefront no los está respetando, un slide con fecha vencida sigue apareciendo. Es el hallazgo más concreto del relevamiento: o se engancha el hook, o se borra el archivo.

2. Los filtros de Algolia no hablan el mismo idioma

Cuatro lugares distintos arman descriptores de filtro con el mismo patrón (filterType + filterValue), pero con valores distintos para las mismas opciones:

Definición Categoría Tag Colección Personalizado
main-navigation y slides category tag collection custom
Bloque productGrid category tag collection index
ctaButton.ts category tag collection index
linkConfig.ts category product_tag collection index

O el storefront contempla las tres variantes, o alguna combinación no está funcionando. Unificarlo rompe las filas ya cargadas, así que se decide con el storefront, no acá.

3. Los tests no cubren nada de lo construido

Los dos tests del repo son los del scaffold, 40 líneas en total: uno pide la lista de usuarios y verifica que la respuesta exista; el otro abre la home y espera encontrar el título Payload Website Template. Ninguno toca banners, sliders, el menú ni la subida de medios.

4. El deploy es manual y no está registrado dónde corre

Hay Dockerfile y docker-compose.yml, pero ningún pipeline de build ni de deploy, y el repo no dice en qué servidor corre en producción. Dos trampas conocidas, documentadas en el runbook:

  • el docker build necesita que la base de datos sea alcanzable, porque Payload se conecta en tiempo de build;
  • las migraciones no se aplican solas — el CMD de la imagen solo levanta el servidor.

5. Nueve campos heredados ocultos, cuatro sin marcar

La convención del repo es ocultar los campos superados en vez de borrarlos, para no romper las filas cargadas. Ya se acumularon nueve: cinco llevan la etiqueta [Obsoleto] y cuatro —los del bloque hero— están ocultos sin ninguna marca.

6. El bloque Slider no exporta su tipo

hero y productGrid declaran interfaceName y el storefront puede importar su tipo; heroSlider no, así que su tipo queda embebido dentro de Page['layout'] en payload-types.ts. Es una línea de configuración.

Operación

Ocho variables de entorno, cuatro dependencias externas y una regla que rompe el build si se olvida.

Dependencia Para qué ¿Qué pasa si no está?
PostgreSQL Toda la persistencia de contenido No arranca — y tampoco compila: Payload se conecta en tiempo de build
Cloudflare Images Almacenamiento y entrega de imágenes Degradación controlada: la subida se saltea con un aviso, el CMS sigue en pie
imagedelivery.net CDN que entrega las variantes Las imágenes no cargan ni en el admin ni en el storefront
Algolia Resuelve los descriptores de filtro El CMS no se entera — nunca lo llama. Las grillas del storefront quedan vacías

Dos detalles que sorprenden a quien llega nuevo:

  • el repo se instala con npm aunque todos los scripts digan pnpm — queda un solo lockfile y el Dockerfile usa npm install --legacy-peer-deps (ADR-0003);
  • la lista de orígenes CORS está hardcodeada en src/payload.config.ts, así que sumar un storefront nuevo es un cambio de código.

Versión visual

Este mismo relevamiento existe como página visual, con el diseño de la serie de inventarios técnicos de Compulandia: https://claude.ai/code/artifact/0dd40d70-5702-4b5a-98ab-add01571880c (privado; requiere sesión de claude.ai del propietario).