Menús para el storefront¶
Lo que la tienda necesita saber para dibujar la navegación que se administra desde este CMS. Escrito para quien implementa el lado del storefront, que es otra persona.
Requisito completo: RF-007. Forma de carga: procedimiento. Historia #299.
Estado. El CMS expone esto; el render todavía no existe. El arreglo
itemsdel global no se quita ni se reemplaza: la barra sigue siendo esa lista, y lo que se agrega es que un ítem pueda desplegar un menú.
Hay una implementación de referencia¶
Antes de escribir nada conviene mirar src/utilities/armarMenu.ts del CMS: arma el árbol con estas
mismas reglas y aplica los recortes. La usa la página /vista-previa-menus, que muestra la
navegación cargada tal como se va a dibujar.
No es código para copiar tal cual —no resuelve destinos ni dibuja nada— pero sí es la lógica de armado y recorte ya escrita y probada contra datos reales.
Las dos consultas¶
Los nombres de campo son ingleses y las etiquetas del panel españolas, por ADR-0007. Lo que sigue usa los nombres que vienen en el JSON.
Todo es lectura pública, sin autenticar. Son dos llamadas y se pueden hacer en paralelo.
GET /api/globals/main-navigation?depth=1
GET /api/menus?limit=0&depth=4
La primera trae la barra: los mismos ítems de siempre, y ahora cada uno puede traer además un
menu. La segunda trae todos los menús, que son decenas, no miles, con sus entradas, sus
categorías y las subcategorías de cada una ya anidadas.
No hay una llamada por nivel ni por categoría. Se traen los menús una vez y el árbol se engancha en memoria por identificador. Con el componente de navegación resuelto en el servidor, eso baja al navegador ya armado.
La profundidad se elige según el catálogo¶
Desde la consulta de menús, la profundidad que pidas es la cantidad de niveles de categoría que recibís con nombre y slug. El salto de la entrada a su categoría ya consume uno, así que no hay que sumarle nada: pedís cuatro, recibís cuatro niveles. Lo que quede más abajo llega como identificador suelto, sin nombre ni slug.
Medido el 2026-09-18 contra un árbol de seis niveles armado a propósito. Las dos primeras filas
se volvieron a medir después de dejar de expandir el parent:
| Profundidad | Niveles con nombre | Respuesta |
|---|---|---|
| 3 | 3 | 5,5 KB |
| 4 | 4 | 7,6 KB |
| 5 | 5 | 31 KB |
| 6 | 6 | 57 KB |
| 7 | 6, el árbol ya se terminó | 135 KB |
Hoy el catálogo llega a cuatro niveles, así que cuatro es el número.
Y mirá la última fila: pedir de más sí cuesta. La profundidad expande todo lo demás también —submenús, imágenes, páginas—, no solo las categorías. Entre cuatro y siete hay casi diez veces de diferencia para el mismo árbol. Conviene pedir lo que se va a mostrar y nada más.
Las subcategorías llegan anidadas¶
Cada categoría trae un campo children, que es un campo de unión: no se guarda, se calcula al
leer a partir de la categoría padre de las demás. Llega así:
{ "name": "Informatica",
"children": { "docs": [ { "name": "Notebooks", "children": { "docs": [...] } } ],
"hasNextPage": false } }
Ojo con dos cosas:
- Es
children.docs, nochildrena secas. Un campo de unión siempre viene paginado. - El
parentde cada hija llega como número, nunca como documento. Es a propósito: si se expandiera, cada hija devolvería a su padre entero y ese padre volvería a traer su propia lista de hijas, engordando el árbol sin aportar nada. Quien baja porchildrenya sabe de quién cuelga cada una. - Vienen en orden alfabético por nombre. No hay orden editorial: si algún día hace falta, se agrega un campo de orden a las categorías y cambia solo esto.
No hace falta pedir las categorías por separado ni engancharlas por identificador: el árbol ya viene armado hasta donde llegue la profundidad.
Campos que llegan y no sirven¶
generateSlugen las categorías: es una casilla del panel para regenerar el slug. Ignorala.hasNextPageenchildren: con el tope sin límite viene siempre en falso.createdAtyupdatedAten todo: sirven para diagnosticar, no para dibujar.
Lo que no vas a encontrar son campos de un tipo dentro de otro: el CMS los limpia al guardar,
así que una entrada propia no trae configuración de categoría y una de categoría no trae link.
Verificado contra la API el 2026-09-18.
Cómo se arma el árbol¶
- La barra son los
itemsdel global, como hasta ahora. Cada ítem trae untype: direct, o vacío en las filas antiguas: se dibuja con sulabely su destino, resuelto igual que hoy.menu: el texto sale delnamedel menú referenciado y el destino, dellinkpropio de ese menú. Ese menú se abre al pasar el mouse y es el primer panel. La barra nunca incluye.
Los campos que no corresponden al tipo llegan en null: un ítem de tipo menu no trae label
ni nada de destino, y uno direct no trae menu. No hace falta interpretarlos ni ignorarlos a
mano. El CMS los enmascara al responder, no los borra, así que el editor puede cambiar de
configuración sin perder lo que había cargado.
2. Dentro de un menú, sus items se dibujan en el orden en que vienen. Los que tienen isVisible
en falso se descartan antes de dibujar.
3. Cada entrada se resuelve según su type:
type |
Qué hacer |
|---|---|
own |
Usar name y resolver link con el mismo código que ya resuelve banners y botones |
category |
La categoría llega entera dentro de la entrada, con sus children.docs. Tomar de ella nombre y destino, y expandir según autoMaxLevels. No mira submenu: una entrada de categoría no lo lleva |
- Si la entrada tiene
submenu, mirar elsubmenuModedel menú que la contiene: conopenno se dibuja hasta que el comprador lo abra; conincludese dibuja acá mismo como sección.
Las dos formas de enganche, que es lo que no se deduce del dato¶
Es la distinción central y conviene tenerla clara antes de escribir el componente.
Cada entrada apunta a un menú con submenu. Cómo se dibuja lo decide submenuMode del menú que
contiene esa entrada, no la entrada:
submenuMode: 'open'— abrir. El menú referenciado va en un panel nuevo, que aparece al pasar el mouse. Suma un nivel.submenuMode: 'include'— incluir. El menú referenciado se dibuja adentro del panel actual, como columna o sección con sunamepor título. No suma nivel. En ese modo la entrada no trae texto ni destino: son campos que el panel del CMS ni siquiera pide.
Hay un simulador interactivo de las dos formas y de la orientación: https://claude.ai/artifact/XP1hCcfSBzL9xhTEzseUhd
Expandir una entrada de categoría¶
autoMaxLevels dice hasta dónde bajar en el árbol de categorías desde la elegida:
| Valor | Resultado |
|---|---|
| vacío | Todos los niveles que tenga el rubro |
0 |
Ninguno. Acceso directo al rubro |
1 |
Las categorías hijas, como líneas simples |
2 o más |
Las hijas pasan a ser títulos de sección y las nietas sus líneas, y así hacia abajo |
Lo generado no tiene un menú del que tomar sus ajustes, así que:
| Qué | De dónde sale |
|---|---|
| Nombre | El name de la categoría. El name de la entrada lo reemplaza |
| Destino | La página de esa categoría |
| Imagen | De la categoría |
| Si se abre o se incluye | autoPresentation de la entrada |
| Hasta dónde bajar | autoMaxLevels de la entrada, o todo si viene vacío |
| Las subcategorías | children.docs de la categoría, ya anidadas |
| Orientación | autoOrientation de la entrada |
Una categoría tiene hoy name, slug, parent e image. El campo icon existe pero está oculto en el panel mientras se decide cómo se cargan los íconos, así que llega siempre vacío. La marca de activa y el
identificador del catálogo llegan con la sincronización, que todavía no existe.
Reglas de dibujo¶
Estas no salen del dato y son las que hacen que el menú se sienta bien.
Orientación. vertical baja en lista, horizontal acomoda en fila. Y decide también por dónde
sale el panel siguiente: un menú horizontal abre sus submenús abajo, ocupando el ancho; uno
vertical los abre al costado. Si no, el panel de una fila horizontal queda fuera de la pantalla.
Altura. El panel acota su altura y se desplaza cuando el contenido la supera, en vez de estirarse fuera de la pantalla.
Retardo. Al entrar, unos 150 ms antes de abrir. Al salir, unos 300 ms antes de cerrar. Sin ese retardo, cruzar el panel en diagonal para llegar a una subcategoría lo cierra en el camino.
Táctil. No hay pasar el mouse. El primer toque sobre una entrada con submenú abre el panel y no navega; para ir a su destino está la opción "Ver todo", que es el destino propio de esa entrada. Sin esta regla el menú es inusable en el celular.
Pantalla angosta. Todo vuelve a lista vertical, sea cual sea la orientación cargada. El mismo árbol alimenta el menú lateral, recorrido por columnas.
Teclado. La navegación se recorre y se abre con el teclado y se cierra con Escape. En el
storefront ya se usa Popover de Headless UI en el menú lateral, que resuelve foco y cierre.
Recortes obligatorios. Salen de las sentencias de RF-007 y hay que aplicarlos al armar el árbol, no confiar en que el CMS los impida:
- Un menú incluido se dibuja como lista vertical, ignorando su propia orientación.
- El nombre de un menú es clickeable si ese menú trae
linkcargado: como título de sección cuando se lo incluye, y como "Ver todo" cuando se lo abre. Sinlink, el nombre va sin enlace. - Las entradas de un menú incluido no abren ni incluyen nada: la inclusión es terminal. Si vienen
con
submenucargado, se ignora. - Un menú que incluye muestra dos niveles y nada más.
- De una entrada de categoría con presentación incluida se toman dos niveles como mucho, aunque
autoMaxLevelspida más. - De una con presentación abierta se toma lo que entre bajo el tope de tres paneles.
Ícono. Llega como un nombre, no como un archivo ni un color. La tienda decide qué dibuja para
cada nombre. Lo mismo va a pasar con los estilos cuando highlight deje de ser una casilla.
Invalidación¶
El CMS avisa al storefront cuando cambia un menú o una categoría, con el mismo patrón que ya usa
Header/hooks/revalidateHeader. Mientras eso no esté, sirve releer cada 60 segundos, que es lo
que hace hoy navigation-service.ts.
Ojo con una diferencia: la frescura ya no depende solo del CMS. Cuando exista la sincronización, un cambio en el catálogo se ve recién después de que corra.
Lo que hay hoy y se reemplaza¶
| Hoy | Después |
|---|---|
getMainNavigation() trae el global |
Sigue igual, y se suman las dos consultas de menús y categorías |
processNavItems() aplana y resuelve URLs |
Sigue igual para los ítems de la barra |
HorizontalNav dibuja botones en fila |
Los mismos botones, y los que traen menu abren un panel |
SideMenu arma su lista con los mismos ítems |
Recorre además el árbol de cada menú, por columnas |
generateNavUrl() sirve tal cual: la gramática de destino no cambia. Lo que hay hoy no se tira:
se le agrega el panel a los ítems que traigan menú.