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) => `
`,
);
}
```
## 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/')
? ``
: ``
}
`
: ''
}
`,
);
}
```
## 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 `