Ir al contenido

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 paneltipo en el JSONToken de la clave
Productos RelacionadosRELATED_PRODUCTSrelated_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
}
{
"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
}
}
}
  • 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_id y variante llegan en null; el precio y la imagen se toman de la primera variante visible, y disponible es true si alguna variante visible lo está.
  • Sin stock no se omite. El ítem llega con disponible: false para 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; items es un array y su orden es el que fijó el comerciante.

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 editable campos.heading; titulo es la columna compartida y hoy va null.
  • Máximo 12 productos. El panel no permite el ítem 13; no asumas más.
  • imagen puede ser null. Renderizá la tarjeta sin imagen; no la descartes por eso.
  • nombre_personalizado es opcional. Cuando el comerciante no renombró la sección, la clave no aparece (no llega como null).
  • La respuesta respeta la cache compartida. Como el resto de las rutas públicas, reenvía If-None-Match con el ETag para ahorrar transferencia — ver Cache, ETag y límites.