Plan de implementación de los menús en el CMS¶
Cómo se construye en este repositorio lo definido en RF-007, en qué orden y cómo se verifica cada paso. Historia #299.
Alcance. Solo el CMS. El render en la tienda lo hace otra persona, con el contrato documentado en Menús para el storefront.
Qué entra y qué no¶
Decisiones tomadas al arrancar, sobre lo que la evaluación de reversibilidad recomendó partir en etapas:
| Entra ahora | Queda para después | |
|---|---|---|
| Menús | Colección completa: entradas propias, incluidas y de categoría | |
| Categorías del catálogo | La colección, con alta y carga a mano | La sincronización desde el catálogo |
| Presentación | highlight como casilla |
Convertirla en lista de estilos con nombre |
| Storefront | El contrato de lectura, documentado | El render |
Que la sincronización no entre ahora tiene una consecuencia de diseño: los campos que después va a escribir el proceso automático hoy se cargan a mano, así que arrancan editables. Cuando llegue la sincronización pasan a solo lectura. El modelo no cambia, cambia quién los escribe.
Convención de nombres¶
Los nombres de campo van en inglés y las etiquetas del panel en español, por
ADR-0007. El motivo corto: cada entrada contiene un
grupo linkConfig, cuyas claves ya son inglesas y se comparten con banners, sliders y botones;
nombrar en español dejaría un mismo objeto mezclado por dentro.
Quien carga contenido no ve estos nombres: ve las etiquetas. El procedimiento de carga no cambia.
Etapas¶
1 · Colección de categorías del catálogo¶
Colección catalog-categories, de lectura pública, separada de categories, que clasifica
entradas del blog y no se toca.
| Campo | Tipo | Nota |
|---|---|---|
name |
texto, requerido | Título del documento |
slug |
único | Con el slugField() del repo, generado desde el nombre |
parent |
relación a sí misma | Jerarquía |
image |
subida → media |
Heredada por los menús |
icon |
select del juego cerrado | Lista en src/fields/iconOptions.ts. Oculto en el panel mientras se decide M6 |
children |
campo de unión | Derivado de parent. Sin tope de hijas, alfabético, y con el tope de recursión holgado: si se lo deja bajo, corta el árbol aunque la consulta pida más profundidad. No agrega columna ni migración |
Arranca con lo mínimo para armar un menú. Los campos de la sincronización se agregan en esa etapa.
Verificación: crear tres categorías anidadas a mano, recuperarlas por API sin autenticar y ver la jerarquía.
2 · Colección de menús¶
Colección menus, de lectura pública, con la estructura de RF-007.
El destino de las entradas propias reutiliza linkConfig(), el campo que ya usan banners y
botones. No se escribe una gramática nueva.
El juego de íconos es una lista cerrada de nombres por significado, no por biblioteca:
notebook, celular, televisor, hogar, oferta y los que hagan falta. El CMS guarda el
nombre y la tienda decide cómo se dibuja, por la misma razón por la que no se guardan colores.
El arreglo de entradas lleva su propio título de fila (src/collections/Menus/RowLabel.tsx), si no
las filas se ven como "Entrada 01" y hay que abrirlas una por una. Agregar un componente del
panel referenciado por texto obliga a correr npm run generate:importmap y commitear el
resultado.
Verificación: cargar un menú con una entrada de cada tipo y ver que el panel muestra solo los campos que corresponden a cada una, y que cada fila se identifica por su nombre.
3 · Reglas de integridad¶
Tres validaciones, las tres del lado del servidor:
- Ciclos. Un menú no puede referenciarse a sí mismo, ni directa ni indirectamente, por
submenu, ni encadenándose por menús incluidos. Se recorre la cadena al guardar. - Tope de tres niveles abiertos. Las inclusiones no cuentan. Se valida al guardar y se vuelve a limitar al dibujar, porque la expansión automática puede empujar la profundidad.
- Borrado de un menú referenciado. Se avisa antes de borrar, en vez de dejar la referencia apuntando a la nada.
Verificación: las tres tienen que fallar al intentarlas. Es la etapa donde una prueba que pasa
no prueba nada: hay que ver el rechazo. Cubierto en tests/int/menus.int.spec.ts, que además
comprueba que incluir no gasta nivel y que un menú que nadie usa sí se puede borrar.
Las tres reglas viven en src/hooks/menuGraph.ts y no en la colección, porque las tres necesitan
mirar el grafo entero. Se puede cargar completo en memoria: los menús son decenas.
4 · Global de navegación¶
Al ítem del global main-navigation se le agrega un campo menu, relación a menus: el menú que
despliega al pasar el mouse. Nada más. La barra, sus ítems, sus destinos y sus etiquetas quedan
como están.
El pie de página no entra: tiene su propio global footer.
Verificación: un ítem sin menú se comporta igual que antes, y uno con menú lo trae en la respuesta de la API.
5 · Sin migración de datos¶
El cambio es aditivo, así que los ítems de la barra se quedan donde están. Lo único pendiente es el desajuste de nombres de RF-005, que el global arrastra desde antes y se corrige por separado.
6 · Contrato para el storefront¶
Documento con lo que la tienda necesita: qué consulta hace, con qué forma vuelve, cómo se arma el árbol y las reglas de dibujo que no se deducen del dato —abrir contra incluir, por dónde sale cada panel, el comportamiento táctil y el de teclado—. Es el entregable para la otra persona.
Verificación: la consulta documentada, corrida contra staging, devuelve el árbol completo.
Vista previa mientras se configura¶
/vista-previa-menus en el propio CMS muestra el árbol de la navegación tal como quedó cargado,
aplicando las mismas sentencias que va a aplicar el storefront: qué se dibuja, qué modo tiene cada
enganche, qué se trae del catálogo y qué se recorta. No es la tienda: es para ver el efecto de
la configuración sin salir del panel.
El armado vive en src/utilities/armarMenu.ts y es, además, la implementación de referencia
para quien haga el storefront. Si cambian las sentencias de RF-007, cambian los dos.
Cómo se prueba cada etapa¶
Cada etapa cerrada actualiza la matriz de verificación de la historia, con una fila por criterio: criterio, prueba y comando. El orden de comprobación es siempre el mismo:
npx tsc --noEmit— que compile.npm run build— que Payload cargue la configuración. Compilar no alcanza: unselectmal armado compila y rompe al arrancar.- Panel en local, contra una base de desarrollo, para ver los campos condicionales.
- La consulta pública sin autenticar, que es el contrato real.
Cuidados propios de este repositorio¶
- Las migraciones se generan, no se escriben a mano, y se agregan al índice de
migrations. Se aplican al arrancar el contenedor, por ADR-0006. migrate:createtambién EMPUJA el esquema a la base a la que se conecta. No solo escribe el archivo: al inicializarse, Payload fuera de producción tiene el push activado, así que la tabla queda creada en la base de desarrollo. Si después se cambia la definición de la colección, el siguientenpm run devencuentra la tabla vieja y se cuelga preguntando por consola si cada columna se crea o se renombra, con opciones todas equivocadas. La salida es borrar la tabla de desarrollo y dejar que el push la vuelva a crear; conviene hacerlo antes de que tenga datos.- El push de drizzle pide confirmación por consola ante advertencias. Sin terminal se cuelga: generar las migraciones en una sesión interactiva.
- Revisar el
downde cada migración de colección nueva. El generador emiteDROP TABLE ... CASCADEy después unDROP CONSTRAINTde la clave foránea que ese mismo CASCADE ya borró, sinIF EXISTS. La vuelta atrás aborta con error 42704 y deja la columna a medio quitar. Apareció en la migración decatalog-categories, que es la primera colección nueva desde que existepayload_locked_documents_rels. Se corrige a mano agregandoIF EXISTSy se deja el motivo escrito en la propia migración. Es la única excepción a la regla de arriba de no escribir migraciones a mano, y vale la pena porque una vuelta atrás rota es difícil de diagnosticar el día que haga falta. npm run buildno pasa en limpio en esta máquina, y no es culpa del cambio que estés haciendo. Con.nextborrado, el build compila y pasa el lint, pero corta al recolectar datos de páginas conPageNotFoundError. Comprobado el 2026-09-17 sobredevelopsin cambios encima, así que es preexistente. Con un.nextde una corrida anterior el build termina en 0 y da falsa tranquilidad. Mientras eso no se resuelva, la verificación de que Payload carga la configuración esnpm run generate:types, que construye la configuración entera y falla si algo está mal armado. En Docker el build sí funciona: el pipeline de staging viene desplegando.- Un campo que deja de ser obligatorio no se puede revertir sin más. Al condicionar un campo
requerido, el generador emite
DROP NOT NULLen elupySET NOT NULLen eldown. Si para entonces hay filas con ese campo vacío, la vuelta atrás falla. Pasó al hacer opcional el texto de los ítems de la barra. Queda anotado en la propia migración. defaultPopulateno convive con un campo de unión. Se probó recortar los campos que viajan de una categoría poblada y la consulta pasó a devolver un error cuando el recorte incluíachildren. El camino que sí funciona para achicar la respuesta esmaxDepth: 0en las relaciones que no hace falta expandir.- Nunca limpiar datos en un gancho de guardado para que la API se vea prolija. Se hizo así y fue destructivo: pasar un menú a modo incluir borraba el texto, el destino y la categoría de todas sus entradas, y volver atrás no los recuperaba. Se perdió contenido real. Lo mismo se consigue enmascarando en la lectura pública, sin tocar la base.
- Regenerar
payload-types.tsdespués de cada cambio de esquema, y commitearlo: es lo que consume el resto del código. - Una opción de
selectrenombrada se migra con los datos, no se reintroduce como obsoleta. - El trabajo va a
develop.stagingse despliega por su propia rama y producción por tag.