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
headeryfooter - 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 buildnecesita que la base de datos sea alcanzable, porque Payload se conecta en tiempo de build; - las migraciones no se aplican solas — el
CMDde 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 usanpm 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).