Productos Relacionados
El comerciante elige hasta 12 productos de su tienda para recomendarlos. A diferencia del
resto de los tipos, acá el tema no resuelve catálogo: el panel guarda solo IDs y la API
entrega cada producto ya resuelto en items[].producto (nombre, precio, URL, imagen,
disponibilidad).
| En el panel | tipo en el JSON | Token de la clave |
|---|---|---|
| Productos Relacionados | RELATED_PRODUCTS | related_products |
{ // Solo en el bundle; el endpoint dedicado no incluye `tipo`. tipo: 'RELATED_PRODUCTS'; titulo: string | null; // hoy siempre null: el título editable vive en campos.heading nombre_personalizado?: string; // nombre interno del panel; la clave se omite si no hay rename items: Array<{ orden: number; // posición fijada por el comerciante campos: {}; // los ítems no tienen campos editables propios producto: { id: string; // ID del producto en Tiendanube variante_id: string | null; // variante elegida; null si el ítem es a nivel producto nombre: string; // nombre en el idioma principal de la tienda variante: string | null; // ej. "Rojo / M"; null sin variante url: string; // URL canónica del producto (nunca ?variant=) imagen: string | null; // thumbnail 480px; null si no hay imagen precio: number | null; // promo si hay promo; null en precio "a consultar" precio_comparacion: number | null;// precio de lista cuando hay promo; si no, null moneda: string | null; // moneda principal de la tienda (ej. "MXN") disponible: boolean; envio_gratis: boolean; marca: string | null; }; }>; campos: { heading: string | null }; // el "Título" de la sección estado: { lifecycle: string; storefrontVisible: boolean }; // solo en el bundle orden: number; indice?: number; // solo cuando hay más de una sección del mismo tipo}Ejemplo
Sección titulada «Ejemplo»{ "sections": { "related_products-product-344443275-cmu7axqfa0001uhc8b8hdznmy": { "titulo": null, "items": [ { "orden": 0, "campos": {}, "producto": { "id": "111111", "variante_id": null, "nombre": "Serum vitamina C", "variante": null, "url": "https://mi-tienda.com/productos/serum-vitamina-c/", "imagen": "https://cdn.example.com/serum-480-0.webp", "precio": 15990, "precio_comparacion": 19990, "moneda": "ARS", "disponible": true, "envio_gratis": true, "marca": "Aurora" } }, { "orden": 1, "campos": {}, "producto": { "id": "222222", "variante_id": "555", "nombre": "Protector solar", "variante": "FPS 50 / 120 ml", "url": "https://mi-tienda.com/productos/protector-solar/", "imagen": null, "precio": null, "precio_comparacion": null, "moneda": "ARS", "disponible": false, "envio_gratis": false, "marca": null } } ], "campos": { "heading": "Combina bien con" }, "estado": { "lifecycle": "active", "storefrontVisible": true }, "orden": 0 } }}Reglas de los ítems
Sección titulada «Reglas de los ítems»- Los productos no públicos se omiten. Un producto despublicado, oculto o eliminado no
llega como ítem; tampoco llega un ítem con todo en
null(a diferencia de otros tipos que sí entregan tarjetas vacías). - Variante que ya no existe → ítem a nivel producto. El producto se sigue mostrando, pero
variante_idyvariantellegan ennull; el precio y la imagen se toman de la primera variante visible, ydisponibleestruesi alguna variante visible lo está. - Sin stock no se omite. El ítem llega con
disponible: falsepara que decidas cómo mostrarlo. - El producto de la página se excluye a sí mismo. En
product/{id}(y en?placement=product&target_id=<id>) un ítem que apunta a ese mismo producto, con cualquier variante, no se devuelve. - Nunca hay costo, stock exacto ni SKU. La única señal de inventario es
disponible. - Puede venir vacía o con menos de 12. El comerciante puede guardar la sección sin
productos;
itemses un array y su orden es el que fijó el comerciante.
Renderizar
Sección titulada «Renderizar»Como cada ítem ya trae producto, no hace falta pedir el catálogo. Aplicá la regla de render
y el orden compartidos y usá los campos directamente:
const res = await fetch( `https://sectionfy.codefy-apps.com/api/public/related-products/${TOKEN}?placement=home`,);const { sections } = await res.json();
const tarjetas = Object.values(sections) .flatMap((seccion) => seccion.items) .sort((a, b) => a.orden - b.orden) .map( ({ producto }) => ` <article class="sectionfy-rp__card" data-product="${producto.id}"> ${ producto.imagen ? `<img src="${producto.imagen}" alt="${escapar(producto.nombre)}" loading="lazy">` : '' } <h3><a href="${producto.url}">${escapar(producto.nombre)}</a></h3> ${producto.variante ? `<p>${escapar(producto.variante)}</p>` : ''} ${ producto.precio === null ? '<p>Consultar precio</p>' : `<p><strong>${formatear(producto.precio, producto.moneda)}</strong> ${ producto.precio_comparacion ? `<s>${formatear(producto.precio_comparacion, producto.moneda)}</s>` : '' }</p>` } </article> `, ) .join('');
seccionContenedor.insertAdjacentHTML('beforeend', `<div class="sectionfy-rp">${tarjetas}</div>`);- El título de la sección no es
titulo. Es el campo editablecampos.heading;tituloes la columna compartida y hoy vanull. - Máximo 12 productos. El panel no permite el ítem 13; no asumas más.
imagenpuede sernull. Renderizá la tarjeta sin imagen; no la descartes por eso.nombre_personalizadoes opcional. Cuando el comerciante no renombró la sección, la clave no aparece (no llega comonull).- La respuesta respeta la cache compartida. Como el resto de las rutas públicas, reenvía
If-None-Matchcon elETagpara ahorrar transferencia — ver Cache, ETag y límites.
