Saltar a contenido

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 items del 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.

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, no children a secas. Un campo de unión siempre viene paginado.
  • El parent de 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 por children ya 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

  • generateSlug en las categorías: es una casilla del panel para regenerar el slug. Ignorala.
  • hasNextPage en children: con el tope sin límite viene siempre en falso.
  • createdAt y updatedAt en 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

  1. La barra son los items del global, como hasta ahora. Cada ítem trae un type:
  2. direct, o vacío en las filas antiguas: se dibuja con su label y su destino, resuelto igual que hoy.
  3. menu: el texto sale del name del menú referenciado y el destino, del link propio 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
  1. Si la entrada tiene submenu, mirar el submenuMode del menú que la contiene: con open no se dibuja hasta que el comprador lo abra; con include se 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 su name por 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 link cargado: como título de sección cuando se lo incluye, y como "Ver todo" cuando se lo abre. Sin link, el nombre va sin enlace.
  • Las entradas de un menú incluido no abren ni incluyen nada: la inclusión es terminal. Si vienen con submenu cargado, 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 autoMaxLevels pida 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ú.