Ir al contenido

Beneficios Destacados

Tarjetas de “beneficios o testimonios destacados”: una imagen o video, un título corto y una estadística con su descripción (ej. 86% → “reportó un acabado más saludable”). El comerciante carga hasta 3 tarjetas por sección, más un título principal.

En el paneltipo en el JSONToken de la clave
Beneficios DestacadosFEATURED_BENEFITSfeatured_benefits

Este tipo usa el sobre genérico de los tipos generados: los campos editables viajan dentro de campos, no en propiedades con nombre propio.

{
titulo: string | null; // hoy siempre null: el título editable vive en campos.heading
nombre_personalizado?: string; // nombre interno del panel (opcional)
items: Array<{
orden: number;
campos: {
imageUrl: string | null; // imagen o video; null si no hay archivo o no está READY
imageMimeType: string | null; // "image/webp", "video/mp4", …; null sin archivo
imageDurationSeconds?: number; // solo videos READY con duración registrada; si no, la clave no viene
cardTitle: string | null;
statValue: string | null; // "86%", "2×", …
statDescription: string | null;
};
}>;
campos: {
heading: string | null; // el "Título principal" de la sección
};
estado: { lifecycle: string; storefrontVisible: boolean };
orden: number;
indice?: number; // solo cuando hay más de una sección del mismo tipo
}

Los items vienen sin filtrar: pueden traer tarjetas con todo en null (el panel permite guardar una tarjeta vacía). Filtrá como en el ejemplo.

{
"sections": {
"featured_benefits-product-344443275-cmu7axqfa0001uhc8b8hdznmy": {
"titulo": null,
"items": [
{
"orden": 0,
"campos": {
"imageUrl": "https://cdn.example.com/serum.mp4",
"imageMimeType": "video/mp4",
"imageDurationSeconds": 12,
"cardTitle": "Glow sin maquillaje",
"statValue": "86%",
"statDescription": "reportó un acabado más saludable desde la primera semana."
}
}
],
"campos": { "heading": "Por qué este serum entra en la rutina" },
"estado": { "lifecycle": "active", "storefrontVisible": true },
"orden": 0
}
}
}

La regla de render y el orden son los compartidos. Lo propio de este tipo es resolver campos y tolerar imageUrl: null:

const res = await fetch(
`https://sectionfy.codefy-apps.com/api/public/content/${TOKEN}/product/${PRODUCT_ID}`,
);
const { sections } = await res.json();
const beneficios = Object.entries(sections)
.filter(([, s]) => s.tipo === 'FEATURED_BENEFITS')
.filter(([, s]) => s.estado.lifecycle === 'active' && s.estado.storefrontVisible)
.sort(([, a], [, b]) => a.orden - b.orden);
for (const [key, seccion] of beneficios) {
const tarjetas = seccion.items
.slice()
.sort((a, b) => a.orden - b.orden)
.filter((item) => item.campos.cardTitle || item.campos.statValue)
.map(
(item) => `
<article class="sectionfy-fb__card" data-item="${item.orden}">
${
!item.campos.imageUrl
? ''
: item.campos.imageMimeType?.startsWith('video/')
? `<video src="${item.campos.imageUrl}" muted loop playsinline autoplay></video>`
: `<img src="${item.campos.imageUrl}" alt="${escapar(item.campos.cardTitle ?? '')}"
loading="lazy">`
}
${item.campos.cardTitle ? `<h3>${escapar(item.campos.cardTitle)}</h3>` : ''}
${
item.campos.statValue
? `<p class="sectionfy-fb__stat">
<strong>${escapar(item.campos.statValue)}</strong>
${escapar(item.campos.statDescription ?? '')}
</p>`
: ''
}
</article>
`,
)
.join('');
contenedor.insertAdjacentHTML(
'beforeend',
`<section class="sectionfy-fb" data-block="${key}">
${seccion.campos.heading ? `<h2>${escapar(seccion.campos.heading)}</h2>` : ''}
<div class="sectionfy-fb__grid">${tarjetas}</div>
</section>`,
);
}
  • El título principal no es titulo. El campo editable por el comerciante llega en campos.heading; titulo es la columna compartida y hoy va null.
  • Máximo 3 tarjetas. El panel bloquea la cuarta; no asumas más de tres items.
  • Placements home, category y product. En el endpoint propio category y product piden target_id (si falta, 400) y home no lo admite; las secciones generales de category/product llegan por el bundle (/{page} o /{page}/{id}).
  • imageUrl es null mientras el asset no esté READY. Renderizá la tarjeta sin imagen en ese caso, no la descartes.
  • La imagen de la tarjeta puede ser un video. Decidí <img> o <video> por imageMimeType (video/…), no por la extensión de la URL. imageDurationSeconds es aditivo: solo llega para videos y nunca para imágenes.