Referencia de secciones: El shape JSON de cada tipo de sección que puede aparecer en el bundle. # Tipos de sección > Los tipos de sección que puede devolver la API, con la equivalencia entre el nombre que ve el comerciante y el identificador que devuelve el JSON. El comerciante arma las secciones desde un panel donde cada tipo tiene un nombre en castellano. La API las devuelve con un identificador en minúsculas y guiones bajos. **Son la misma cosa con dos nombres**, y esta tabla es la traducción. ## La tabla [Sección titulada «La tabla»](#la-tabla) | En el panel | `tipo` en el JSON | Token de la clave | Documentación | | --------------------------------- | ----------------------- | ----------------------- | -------------------------------------------------------------------- | | Preguntas Frecuentes | `FAQ` | `faq` | [Ver](/docs/referencia/secciones/preguntas-frecuentes/) | | Preguntas con imagen | `FAQ_WITH_IMAGE` | `faq_with_image` | [Ver](/docs/referencia/secciones/preguntas-con-imagen/) | | Carrusel de imágenes | `IMAGE_CAROUSEL` | `image_carousel` | [Ver](/docs/referencia/secciones/carrusel-de-imagenes/) | | Carrusel de Especificaciones | `SPECS` | `specs` | [Ver](/docs/referencia/secciones/carrusel-de-especificaciones/) | | FAQS de Especificaciones | `SPECS_FAQ` | `specs_faq` | [Ver](/docs/referencia/secciones/faqs-de-especificaciones/) | | Banner Características Técnicas | `RICH_DESCRIPTION` | `rich_description` | [Ver](/docs/referencia/secciones/banner-caracteristicas-tecnicas/) | | Grid Promocional | `GRID_PROMOCIONAL` | `grid_promocional` | [Ver](/docs/referencia/secciones/grid-promocional/) | | Call to Action Banner | `CALL_TO_ACTION_BANNER` | `call_to_action_banner` | [Ver](/docs/referencia/secciones/call-to-action-banner/) | | Banner de Características Clave | `KEY_FEATURES_BANNER` | `key_features_banner` | [Ver](/docs/referencia/secciones/banner-de-caracteristicas-clave/) | | Banner Promocional | `PROMO_MODAL` | `promo_modal` | [Ver](/docs/referencia/secciones/promo-modal/) | | Beneficios Destacados | `FEATURED_BENEFITS` | `featured_benefits` | [Ver](/docs/referencia/secciones/beneficios-destacados/) | | Productos relacionados | `RELATED_PRODUCTS` | `related_products` | [Ver](/docs/referencia/secciones/related-products/) | | Video Banner | `VIDEO_BANNER` | `video_banner` | [Ver](/docs/referencia/secciones/video-banner/) | | Parallax Banner | `PARALLAX_BANNER` | `parallax_banner` | [Ver](/docs/referencia/secciones/parallax-banner/) | | Banner de Garantía | `WARRANTY_BANNER` | `warranty_banner` | [Ver](/docs/referencia/secciones/banner-de-garantia/) | | Preguntas frecuentes del producto | `PRODUCT_FAQS` | `product_faqs` | [Ver](/docs/referencia/secciones/preguntas-frecuentes-del-producto/) | | Carrusel de testimonios en video | `VIDEO_REVIEWS` | `video_reviews` | [Ver](/docs/referencia/secciones/carrusel-de-testimonios-en-video/) | Las dos columnas del medio son el mismo identificador en dos convenciones: `MAYUSCULAS_CON_GUION_BAJO` en el campo `tipo`, `minusculas_con_guion_bajo` en la clave de `sections`. Para despachar por tipo usa `tipo` — el detalle está en [la estructura de la respuesta](/docs/referencia/estructura-respuesta/#identificar-el-tipo-de-una-secci%C3%B3n). ## Lo que comparten todos [Sección titulada «Lo que comparten todos»](#lo-que-comparten-todos) Sin importar el tipo, toda sección trae los mismos campos comunes —`tipo`, `titulo`, `items`, `estado`, `orden`, `indice`— y se rige por las mismas dos reglas: 1. **La regla de render.** Muéstrala solo si `estado.lifecycle === 'active' && estado.storefrontVisible === true`. 2. **El orden.** Ordénala por `orden` entre secciones, y los `items` por su propio `orden`. Las dos están explicadas una sola vez en [la estructura de la respuesta](/docs/referencia/estructura-respuesta/). Las páginas de aquí abajo no las repiten: se concentran en lo que cada tipo tiene de particular. ## Cómo leer estas páginas [Sección titulada «Cómo leer estas páginas»](#cómo-leer-estas-páginas) Cada página tiene la misma estructura: * **Shape** — los campos, marcando qué puede ser `null` y qué puede faltar. * **Ejemplo** — un JSON real, no inventado. * **Renderizar** — solo la parte específica del tipo. El fetch, el filtro y el escape de texto son compartidos y viven en [conectar una sección custom](/docs/guias/seccion-custom/#1-el-m%C3%B3dulo-cliente). * **Notas** — las trampas propias de ese tipo. Los ejemplos asumen dos helpers de ese módulo compartido: `seccionesDeTipo(bundle, kind)`, que ya aplica la regla de render y el orden, y `escapar(texto)`, que evita que el contenido del comerciante se inyecte como HTML. # Banner Características Técnicas > Tarjeta compuesta con imagen, texto, lista de características y botones de acción — shape, ejemplo JSON y cómo renderizarla. El tipo más completo: imagen principal, párrafo, lista de características con íconos, y botones de acción configurables. | En el panel | `tipo` en el JSON | Token de la clave | | ----------------------------------- | ------------------ | ------------------ | | **Banner Características Técnicas** | `RICH_DESCRIPTION` | `rich_description` | ## Shape [Sección titulada «Shape»](#shape) ```ts { tipo: 'RICH_DESCRIPTION'; titulo: string | null; texto: string | null; // párrafo principal imagen: { url: string; alt: string | null } | null; // imagen hero items: Array<{ // lista de características texto: string; icono: string | null; // URL suelta, no objeto orden: number; }>; acciones?: Array; // opcional — ver abajo } ``` `items[].icono` es una URL suelta En este tipo el ícono de cada característica es un **string**, no un objeto `{ url, alt }` como en el resto de las secciones. Es la excepción del contrato. ### Las acciones [Sección titulada «Las acciones»](#las-acciones) ```ts type Accion = { tipo: 'guia_tallas' | 'manual' | 'manual-uso'; modo: 'image' | 'link' | 'pdf'; etiqueta: string | null; // Presente según `modo`: imagen?: { url: string; alt: string | null }; // modo === 'image' enlace?: { // modo === 'link' url: string; etiqueta: string | null; nueva_pestana: boolean; }; pdf?: { url: string }; // modo === 'pdf' }; ``` `modo` dice **cómo entregar** el recurso y qué campo leer. `tipo` dice **qué es**, para poner una etiqueta por defecto cuando `etiqueta` es `null`. | `modo` | Campo presente | Render sugerido | | ------- | -------------- | ----------------------------- | | `image` | `imagen` | Abrir en modal o lightbox. | | `link` | `enlace` | Un `` común. | | `pdf` | `pdf` | Un `` que abre o descarga. | ## Ejemplo [Sección titulada «Ejemplo»](#ejemplo) ```json { "rich_description-product-333333174-cmqic8fcm000ueq2b5gm7zg8q": { "tipo": "RICH_DESCRIPTION", "titulo": "Características técnicas", "texto": "Diseñada para uso diario intensivo, con materiales de grado industrial.", "imagen": { "url": "https://cdn.example.com/hero.jpg", "alt": "Producto sobre mesa de trabajo" }, "items": [ { "texto": "Resistente al agua IPX7", "icono": "https://cdn.example.com/agua.svg", "orden": 0 }, { "texto": "Batería de 18 horas", "icono": null, "orden": 1 } ], "acciones": [ { "tipo": "guia_tallas", "modo": "image", "imagen": { "url": "https://cdn.example.com/tallas.png", "alt": null }, "etiqueta": null }, { "tipo": "manual", "modo": "pdf", "pdf": { "url": "https://cdn.example.com/manual.pdf" }, "etiqueta": "Descargar manual" } ], "estado": { "lifecycle": "active", "storefrontVisible": true }, "orden": 1 } } ``` ## Renderizar [Sección titulada «Renderizar»](#renderizar) Lo específico del tipo es el despacho de las acciones: ```js // Etiquetas por defecto cuando el comerciante no puso una. const ETIQUETAS = { guia_tallas: 'Guía de tallas', manual: 'Manual', 'manual-uso': 'Manual de uso', }; function renderAccion(accion) { const texto = escapar(accion.etiqueta ?? ETIQUETAS[accion.tipo] ?? 'Ver más'); switch (accion.modo) { case 'image': // No es un enlace: abre un modal con la imagen. return ``; case 'link': return ` ${escapar(accion.enlace.etiqueta ?? accion.etiqueta ?? ETIQUETAS[accion.tipo])} `; case 'pdf': return `${texto}`; default: // Modo desconocido: no romper, simplemente no renderizar. return ''; } } ``` Y la lista de características, donde el ícono es decorativo porque el texto de al lado ya dice lo mismo: ```js function renderCaracteristica(item) { return `
  • ${item.icono ? `` : ''} ${escapar(item.texto)}
  • `; } ``` ## Notas [Sección titulada «Notas»](#notas) * **`acciones` puede no venir.** Es opcional: usa `seccion.acciones ?? []`. * **Despacha por `modo`, no por `tipo`.** Leer `accion.enlace` cuando `modo` es `"image"` da `undefined`. * Incluye un caso `default` en el switch: si mañana aparece un `modo` nuevo, tu tema lo ignora en vez de romperse. # Banner de Características Clave > Beneficios de la tienda con ícono — el único tipo con endpoint propio. Beneficios a nivel tienda con ícono, del estilo “envío gratis / cuotas sin interés / cambios sin cargo”. | En el panel | `tipo` en el JSON | Token de la clave | | ----------------------------------- | --------------------- | --------------------- | | **Banner de Características Clave** | `KEY_FEATURES_BANNER` | `key_features_banner` | Tiene endpoint propio Este tipo **no llega en el bundle de contenido**. Se pide aparte: ```bash curl https://sectionfy.codefy-apps.com/api/public/key-features-banner/TU_TOKEN ``` Si lo buscas en la respuesta de `/api/public/content/...`, no está. ## La respuesta [Sección titulada «La respuesta»](#la-respuesta) La respuesta es más simple que la del bundle: solo `sections`, sin `page` ni identificador de entidad. ```ts { sections: Record; } ``` Como no hay `page`, **no puedes derivar el placement de la respuesta**. Si necesitas saber dónde ubicó el comerciante cada banner, léelo del segundo segmento de la clave. ## Shape de la sección [Sección titulada «Shape de la sección»](#shape-de-la-sección) ```ts { titulo: string | null; texto: string | null; imagen: { url: string; alt: string | null } | null; // imagen de la sección items: Array<{ imagen: { url: string; alt: string | null } | null; titulo: string | null; // nullable, a diferencia de SPECS descripcion: string | null; // nullable, a diferencia de SPECS orden: number; }>; } ``` Los items son casi idénticos a los de [Carrusel de Especificaciones](/docs/referencia/secciones/carrusel-de-especificaciones/), con una diferencia que importa: aquí **`titulo` y `descripcion` pueden ser `null`**. El panel permite guardar una característica con el encabezado o el cuerpo vacío, y esos casos llegan como `null` en vez de omitirse. ## Ejemplo [Sección titulada «Ejemplo»](#ejemplo) ```json { "sections": { "key_features_banner-home-general-cmr9p2xkd0004hs5bn3wq7t1e": { "titulo": "Por qué comprarnos", "nombre_personalizado": "Banner home", "texto": "Tres razones para elegirnos.", "imagen": null, "items": [ { "imagen": { "url": "https://cdn.example.com/envio.svg", "alt": "Envío" }, "titulo": "Envío gratis", "descripcion": "En compras superiores a $50.000", "orden": 0 }, { "imagen": { "url": "https://cdn.example.com/cuotas.svg", "alt": null }, "titulo": "12 cuotas sin interés", "descripcion": null, "orden": 1 } ], "estado": { "lifecycle": "active", "storefrontVisible": true }, "orden": 0 } } } ``` ## Renderizar [Sección titulada «Renderizar»](#renderizar) La respuesta no trae `page`, así que el helper `seccionesDeTipo` no aplica tal cual. Filtra directo sobre `sections`, con la misma regla de render: ```js const res = await fetch( `https://sectionfy.codefy-apps.com/api/public/key-features-banner/${TOKEN}`, ); const { sections } = await res.json(); const banners = Object.entries(sections) .filter(([, s]) => s.estado.lifecycle === 'active' && s.estado.storefrontVisible) .sort(([, a], [, b]) => a.orden - b.orden); for (const [key, banner] of banners) { const caracteristicas = banner.items .slice() .sort((a, b) => a.orden - b.orden) // Una característica sin título ni descripción no aporta nada: omitirla. .filter((item) => item.titulo || item.descripcion) .map( (item) => `
  • ${ item.imagen ? `${escapar(item.imagen.alt ?? '')}` : '' } ${item.titulo ? `

    ${escapar(item.titulo)}

    ` : ''} ${item.descripcion ? `

    ${escapar(item.descripcion)}

    ` : ''}
  • `, ) .join(''); contenedor.insertAdjacentHTML( 'beforeend', `
    ${banner.titulo ? `

    ${escapar(banner.titulo)}

    ` : ''} ${banner.texto ? `

    ${escapar(banner.texto)}

    ` : ''}
      ${caracteristicas}
    `, ); } ``` ## Notas [Sección titulada «Notas»](#notas) * **Filtra los items vacíos.** Un item con `titulo` y `descripcion` en `null` renderiza una tarjeta en blanco. * Es una llamada más, aparte del bundle. Si tu página necesita las dos cosas, dispáralas en paralelo — ver [recetas de fetch](/docs/guias/recetas-fetch/#dos-endpoints-en-paralelo). # Banner de Garantía > Banner de producto con sello de garantía, imagen o video y botón — shape, ejemplo JSON y cómo renderizarlo. Banner de garantía de producto: título, descripción, una imagen o video y un botón opcional, más un sello circular con el texto de garantía. El comerciante lo arma una vez y lo ubica en la **home**, en **todas** las categorías o productos, o en una página de producto puntual. | En el panel | `tipo` en el JSON | Token de la clave | | ---------------------- | ----------------- | ----------------- | | **Banner de Garantía** | `WARRANTY_BANNER` | `warranty_banner` | ## Shape de la sección [Sección titulada «Shape de la sección»](#shape-de-la-sección) Este tipo usa el sobre genérico de los tipos generados: los campos editables viajan dentro de `campos`, no en propiedades con nombre propio. ```ts { tipo: 'WARRANTY_BANNER'; // identificador del tipo en el bundle titulo: string | null; // columna compartida; hoy puede venir null nombre_personalizado?: string; // nombre interno del panel (opcional) items: []; // siempre vacío: el contenido vive a nivel sección campos: { productTitle: string | null; productDescription: string | null; productImageUrl: string | null; // imagen o video; null si no hay archivo o no está READY productImageMimeType: string | null; // "image/webp", "video/mp4", …; null sin archivo productImageDurationSeconds?: number; // solo videos READY con duración registrada; si no, la clave no viene warrantyButtonUrl: string | null; warrantyButtonLabel: string | null; warrantySealText: string | null; }; estado: { lifecycle: string; storefrontVisible: boolean }; orden: number; indice?: number; // solo cuando hay más de una sección del mismo tipo } ``` `items` viene como array vacío, por consistencia con el resto de las secciones. Este tipo no lo usa: no lo recorras. ## Ejemplo [Sección titulada «Ejemplo»](#ejemplo) ```json { "sections": { "warranty_banner-product-344443275-cmu7axqfa0001uhc8b8hdznmy": { "tipo": "WARRANTY_BANNER", "titulo": null, "items": [], "campos": { "productTitle": "Botas de presoterapia PRO 3", "productDescription": "Botas de presoterapia con 5 años de garantía oficial.", "productImageUrl": "https://cdn.example.com/botas-pro-3.jpg", "productImageMimeType": "image/jpeg", "warrantyButtonUrl": "https://mitienda.com/productos/botas-pro-3/", "warrantyButtonLabel": "Ver producto", "warrantySealText": "5 años de garantía" }, "estado": { "lifecycle": "active", "storefrontVisible": true }, "orden": 0 } } } ``` ## Renderizar [Sección titulada «Renderizar»](#renderizar) Todos los `campos` pueden ser `null`, así que cada parte es condicional: ```js const res = await fetch( `https://sectionfy.codefy-apps.com/api/public/content/${TOKEN}/product/${PRODUCT_ID}`, ); const { sections } = await res.json(); const garantias = Object.entries(sections) .filter(([, s]) => s.tipo === 'WARRANTY_BANNER') .filter(([, s]) => s.estado.lifecycle === 'active' && s.estado.storefrontVisible) .sort(([, a], [, b]) => a.orden - b.orden); for (const [key, seccion] of garantias) { const { campos } = seccion; contenedor.insertAdjacentHTML( 'beforeend', `
    ${ !campos.productImageUrl ? '' : campos.productImageMimeType?.startsWith('video/') ? `` : `${escapar(campos.productTitle ?? '')}` }
    ${campos.productTitle ? `

    ${escapar(campos.productTitle)}

    ` : ''} ${campos.productDescription ? `

    ${escapar(campos.productDescription)}

    ` : ''} ${ campos.warrantyButtonUrl ? ` ${escapar(campos.warrantyButtonLabel ?? 'Ver producto')} ` : '' }
    ${ campos.warrantySealText ? `
    ${escapar(campos.warrantySealText)}
    ` : '' }
    `, ); } ``` ## Notas [Sección titulada «Notas»](#notas) * **El sello es siempre circular.** El comerciante solo carga el texto (`warrantySealText`); el círculo, el borde y la tipografía son decisión de tu tema. * **La imagen puede ser un video.** Decidí `` o `