Ir al contenido

Estructura de la respuesta

Todos los endpoints devuelven la misma estructura. Cambia el identificador de la página; el resto es idéntico.

{
"page": "product",
"product_id": "333333174",
"sections": {}
}
CampoTipoPresente en
pagehome · category · productTodas las respuestas de contenido.
product_idstringSolo cuando page es product y pediste un producto concreto.
category_idstringSolo cuando page es category y pediste una categoría concreta.
sectionsobjetoSiempre. Puede venir vacío ({}).

sections es un objeto indexado por clave, no un array. Si no hay contenido configurado, es {} — nunca null.

Cada clave de sections codifica cuatro datos:

faq_with_image-product-333333174-cmqic4kt5000ieq2bk3hbiat3
└──────┬─────┘ └──┬──┘ └───┬────┘ └────────────┬─────────┘
│ │ │ │
kind placement targetId blockId
ParteQué es
kindEl tipo de sección, en minúsculas y con guiones bajos: faq, faq_with_image, image_carousel, rich_description, grid_promocional, call_to_action_banner, specs, specs_faq, key_features_banner, promo_modal, featured_benefits, video_banner, parallax_banner, video_reviews.
placementLa página donde vive. Solo tres valores: home, category, product — los mismos que page.
targetIdEl ID de la entidad, o el literal general cuando la sección no apunta a una entidad concreta.
blockIdIdentidad estable de esa instancia. Es un cuid.

No cambia cuando el comerciante reordena las secciones ni cuando borra otra. Puedes usarlo como key de render, como ancla de URL o como identificador de caché sin que se rompa en la próxima carga.

Hay dos formas. Usa la primera.

Cada sección trae un discriminador explícito con el valor crudo del tipo, en mayúsculas:

{
"tipo": "RICH_DESCRIPTION",
"titulo": "Características técnicas",
"items": []
}

Eso te deja despachar sin tocar la clave:

const renderers = {
FAQ: renderFaq,
FAQ_WITH_IMAGE: renderFaqConImagen,
IMAGE_CAROUSEL: renderCarrusel,
RICH_DESCRIPTION: renderDescripcion,
GRID_PROMOCIONAL: renderGrilla,
CALL_TO_ACTION_BANNER: renderCta,
SPECS: renderSpecs,
SPECS_FAQ: renderSpecsFaq,
KEY_FEATURES_BANNER: renderCaracteristicas,
PROMO_MODAL: renderPromo,
FEATURED_BENEFITS: renderBeneficios,
VIDEO_BANNER: renderVideoBanner,
PARALLAX_BANNER: renderParallaxBanner,
VIDEO_REVIEWS: renderTestimonios,
};
for (const seccion of visiblesEnOrden(bundle)) {
renderers[seccion.tipo]?.(seccion);
}

Observa que tipo usa MAYUSCULAS_CON_GUION_BAJO mientras que el kind de la clave usa minusculas_con_guion_bajo. Son el mismo valor en dos convenciones.

Si necesitas el tipo a partir de la clave, la única forma correcta es la comparación por token exacto:

// ❌ MAL — `faq` también matchea `faq_with_image`
Object.keys(bundle.sections).filter((k) => k.startsWith('faq'));
// ✅ BIEN — comparación por token exacto
Object.keys(bundle.sections).filter((k) => k.split(`-${bundle.page}-`)[0] === 'faq');

El separador que usas para cortar es -{placement}-, no un guion suelto: los propios kind llevan guiones bajos. Cortar por el placement completo es lo único que desambigua — y como bundle.page es exactamente el placement, no cuesta nada.

El helper seccionesDeTipo(bundle, kind) que aparece en los ejemplos de toda la documentación combina las dos vías —tipo primero, clave como respaldo— y de paso aplica la regla de render y el orden. Está escrito una sola vez, en el módulo cliente.

Sin importar el tipo, toda sección trae estos tres:

CampoTipoSignificado
tipostringDiscriminador del tipo, en mayúsculas (FAQ, RICH_DESCRIPTION, …).
estado{ lifecycle, storefrontVisible }La compuerta de render. Ver abajo.
ordennumberPosición entre secciones de todos los tipos. Es el orden que el comerciante armó arrastrando en el panel.
indicenumber | ausenteNúmero de instancia (1-based) dentro del mismo tipo y alcance. Solo aparece cuando hay dos o más.
titulostring | nullTítulo que puso el comerciante.
nombre_personalizadostring | ausenteNombre con el que el comerciante renombró la sección. Aparece solo si la renombró.
itemsarraySiempre presente. Puede estar vacío, nunca es null.

titulo y nombre_personalizado no son lo mismo

Sección titulada «titulo y nombre_personalizado no son lo mismo»

titulo es el encabezado que se muestra en la tienda. nombre_personalizado es la etiqueta interna con la que el comerciante identifica esa sección en su panel cuando tiene varias del mismo tipo.

Para renderizar usa titulo. nombre_personalizado te sirve si estás armando una herramienta de previsualización o un selector, no para la vitrina.

Si el comerciante dejó un carrusel arriba de un FAQ, el carrusel tiene orden menor. Para respetar la maquetación que armó, ordena todas las secciones juntas por orden y solo entonces despacha por tipo:

export function visiblesEnOrden(bundle) {
return Object.entries(bundle.sections)
.filter(([, s]) => s.estado.lifecycle === 'active' && s.estado.storefrontVisible)
.sort(([, a], [, b]) => a.orden - b.orden)
.map(([key, seccion]) => ({ key, ...seccion }));
}
for (const seccion of visiblesEnOrden(bundle)) {
renderizar(seccion.tipo, seccion);
}

La respuesta ya viene emitida en orden ascendente de orden, así que en la práctica el orden de inserción del objeto coincide. Aun así ordena explícitamente: depender del orden de claves de un objeto JSON es frágil, y cualquier JSON.parse intermedio o serialización de terceros puede alterarlo.

Cuando hay un solo FAQ en un producto, indice no viene. Cuando hay dos, el primero trae indice: 1 y el segundo indice: 2. Es lo que usas para diferenciarlos en la UI si no tienen título propio.

La respuesta es un almacén completo: incluye a propósito secciones que el comerciante ocultó o archivó, para que existan herramientas de previsualización y para que apagar contenido no signifique perderlo.

lifecyclestorefrontVisible¿Renderizar?
activetrueSí
activefalseNo — el comerciante la ocultó
archivedtrueNo — está archivada
archivedfalseNo

Las secciones eliminadas de forma permanente no aparecen en la respuesta.

CampoPuede ser nullQué hacer
tituloSíNo renderizar el encabezado.
textoSíOmitir el párrafo.
imagenSíOmitir el <img>. Solo se incluyen los assets que terminaron de procesarse.
imagen.altSíUsar cadena vacía para decorativas.
enlace / botonSíNo renderizar el CTA.
itemsNoSiempre array. Puede estar vacío.

Un patrón que evita casi todos los errores en runtime:

const src = seccion.imagen?.url;
if (src) contenedor.append(crearImagen(src, seccion.imagen.alt ?? ''));

El shape concreto de cada tipo está en tipos de sección.