Ir al contenido

Preguntas frecuentes del producto

Sección de preguntas frecuentes del producto: un título, hasta 10 pares pregunta/respuesta y una galería de hasta 15 imágenes y videos con un archivo principal. El comerciante la ubica en una página de producto puntual.

En el paneltipo en el JSONToken de la clave
Preguntas frecuentes del productoPRODUCT_FAQSproduct_faqs

Este tipo usa el sobre genérico de los tipos generados: los campos editables viajan dentro de campos y las preguntas dentro de items.

{
tipo: 'PRODUCT_FAQS'; // identificador del tipo en el bundle
titulo: string | null; // columna compartida; hoy puede venir null
nombre_personalizado?: string; // nombre interno del panel (opcional)
items: Array<{
orden: number; // posición de la pregunta (0-based)
campos: {
question: string | null; // la pregunta
answer: string | null; // la respuesta
};
}>;
campos: {
productFaqsTitle: string | null; // título del producto
productFaqsGallery: { // galería (hasta 15), en orden de presentación
items: Array<{
url: string | null; // imagen o video (URL pública del archivo)
mimeType: string | null; // "image/webp", "video/mp4", "video/webm"
durationSeconds?: number; // solo videos READY con duración registrada
position: number; // orden dentro de la galería
isPrimary: boolean; // exactamente un item es el principal
}>;
};
// Campos anteriores, ahora DERIVADOS de la galería (compatibilidad con temas existentes):
productFaqsMainImageUrl: string | null; // el item principal (o el primero si ninguno lo es)
productFaqsMainImageMimeType: string | null; // "image/webp", "video/mp4", …; null sin archivo
productFaqsMainImageDurationSeconds?: number; // solo videos READY con duración registrada
productFaqsCollageImage1Url: string | null; // los 4 items siguientes, en orden y sin repetir el principal
productFaqsCollageImage1MimeType: string | null;
productFaqsCollageImage1DurationSeconds?: number;
// …y lo mismo para CollageImage2, CollageImage3 y CollageImage4
};
estado: { lifecycle: string; storefrontVisible: boolean };
orden: number;
indice?: number; // solo cuando hay más de una sección del mismo tipo
}

La numeración de preguntas (“Pregunta 1”, “Respuesta 1”) es solo del panel del comerciante. No viaja en el JSON: el número se deriva del orden del items. No lo persistas ni lo muestres en el storefront salvo que tu diseño lo pida.

{
"sections": {
"product_faqs-product-344443275-cmv16uy7d0001am0stxnh8zuv": {
"tipo": "PRODUCT_FAQS",
"titulo": null,
"items": [
{
"orden": 0,
"campos": {
"question": "¿Es apto para veganos?",
"answer": "Sí, todos nuestros cereales son 100% de origen vegetal."
}
},
{
"orden": 1,
"campos": {
"question": "¿Cuánto dura la frescura?",
"answer": "12 meses cerrado; una vez abierto, consúmelo en 2 semanas."
}
}
],
"campos": {
"productFaqsTitle": "Nuevo Cereal Mexicano!",
"productFaqsGallery": {
"items": [
{
"url": "https://cdn.example.com/cereal-principal.webp",
"mimeType": "image/webp",
"position": 0,
"isPrimary": true
},
{
"url": "https://cdn.example.com/cereal-1.webp",
"mimeType": "image/webp",
"position": 1,
"isPrimary": false
},
{
"url": "https://cdn.example.com/cereal-2.mp4",
"mimeType": "video/mp4",
"durationSeconds": 12,
"position": 2,
"isPrimary": false
}
]
},
"productFaqsMainImageUrl": "https://cdn.example.com/cereal-principal.webp",
"productFaqsMainImageMimeType": "image/webp",
"productFaqsCollageImage1Url": "https://cdn.example.com/cereal-1.webp",
"productFaqsCollageImage1MimeType": "image/webp",
"productFaqsCollageImage2Url": "https://cdn.example.com/cereal-2.mp4",
"productFaqsCollageImage2MimeType": "video/mp4",
"productFaqsCollageImage2DurationSeconds": 12,
"productFaqsCollageImage3Url": null,
"productFaqsCollageImage3MimeType": null,
"productFaqsCollageImage4Url": null,
"productFaqsCollageImage4MimeType": null
},
"estado": { "lifecycle": "active", "storefrontVisible": true },
"orden": 0
}
}
}

La galería puede venir vacía y items también. Renderizá cada parte condicionalmente. Usá productFaqsGallery para temas nuevos; los campos productFaqsMainImage* y productFaqsCollageImage1..4* siguen disponibles y no cambian de nombre:

const res = await fetch(
`https://sectionfy.codefy-apps.com/api/public/content/${TOKEN}/product/${PRODUCT_ID}`,
);
const { sections } = await res.json();
const faqs = Object.entries(sections)
.filter(([, s]) => s.tipo === 'PRODUCT_FAQS')
.filter(([, s]) => s.estado.lifecycle === 'active' && s.estado.storefrontVisible)
.sort(([, a], [, b]) => a.orden - b.orden);
const medio = (url, mime) =>
!url
? ''
: mime?.startsWith('video/')
? `<video src="${url}" muted loop playsinline autoplay></video>`
: `<img src="${url}" loading="lazy" alt="">`;
for (const [key, seccion] of faqs) {
const { campos, items } = seccion;
const galeria = campos.productFaqsGallery.items;
const principal = galeria.find((m) => m.isPrimary) ?? galeria[0];
const resto = galeria.filter((m) => m !== principal);
contenedor.insertAdjacentHTML(
'beforeend',
`<section class="sectionfy-product-faqs" data-block="${key}">
<div class="sectionfy-product-faqs__panel">
${campos.productFaqsTitle ? `<h2>${escapar(campos.productFaqsTitle)}</h2>` : ''}
<dl class="sectionfy-product-faqs__lista">
${[...items]
.sort((a, b) => a.orden - b.orden)
.map(
(item) => `
<details>
<summary>${escapar(item.campos.question ?? '')}</summary>
<p>${escapar(item.campos.answer ?? '')}</p>
</details>`,
)
.join('')}
</dl>
</div>
<div class="sectionfy-product-faqs__principal">
${principal ? medio(principal.url, principal.mimeType) : ''}
</div>
<div class="sectionfy-product-faqs__collage">
${resto.map((m) => medio(m.url, m.mimeType)).join('')}
</div>
</section>`,
);
}
  • Máximo 10 preguntas. El panel bloquea la carga de la número 11; el JSON nunca trae más de 10.
  • La numeración es del panel, no del contrato. “Pregunta 1” / “Respuesta 1” se derivan del orden de items; si tu diseño necesita números, calculalos vos con orden y no los persistas.
  • Pregunta y respuesta siempre viajan juntas. Cada item trae ambas o ninguna (el panel exige las dos).
  • La galería admite hasta 15 imágenes y videos. Los items vienen ordenados por position; exactamente uno tiene isPrimary: true (si la galería no está vacía). productFaqsGallery nunca expone IDs de archivos.
  • Los campos anteriores ahora se derivan de la galería, sin cambio de contrato. productFaqsMainImage* es el item principal (o el primero si ninguno lo es) y productFaqsCollageImage1..4* son los siguientes cuatro items en orden, sin repetir el principal. Si hay menos de cinco archivos, los últimos campos llegan como null.
  • La galería admite hasta 15 archivos; los campos anteriores solo reflejan los primeros cinco. Los extras únicamente están en productFaqsGallery, así que un tema que quiera mostrarlos todos tiene que leer la galería.
  • Las imágenes pueden ser videos. Decidí <img> o <video> por el mimeType / *MimeType (video/…), no por la extensión de la URL. Los durationSeconds / *DurationSeconds son aditivos: solo llegan para videos.
  • Un archivo que no está READY no aparece en la galería. Los campos derivados se compactan: toman los siguientes archivos disponibles por posición, sin dejar un null en el lugar del que falta; solo los últimos campos llegan como null (*Url). Renderizá la sección sin ese archivo, no la descartes.
  • Notas de la migración. Al pasar de los cinco campos de imagen a la galería, los datos existentes se copiaron una sola vez, en el orden principal y collage 1 a 4. Dos casos cambian de comportamiento: si en un bloque la única imagen conservada era un collage, en la galería queda como principal y por eso llega en productFaqsMainImage* (antes seguía siendo un collage); y un archivo que todavía estaba pendiente (no READY) al migrar no se copió y no se incorpora después, aunque más tarde quede listo: hay que volver a agregarlo desde el panel.
  • Placement product solamente. Llega por el bundle de contenido de la página de producto; no tiene endpoint propio.