Conectar una sección custom
Guía completa: un módulo reutilizable, una sección que lo usa, y el cableado en el tema. El ejemplo renderiza FAQs, pero la estructura sirve para cualquier tipo.
1. El módulo cliente
Sección titulada «1. El módulo cliente»Un solo archivo con todo lo que se comparte: el fetch cacheado, el filtro por tipo y la regla de render.
const API = 'https://sectionfy.codefy-apps.com/api/public/content';
/** Cache de promesas en vuelo, compartida por todas las secciones de la página. */const enVuelo = new Map();
/** * Trae el bundle de una página. Llamadas repetidas con la misma clave * comparten un único request. */export function traerBundle(token, page, targetId) { const ruta = targetId ? `${token}/${page}/${targetId}` : `${token}/${page}`;
if (!enVuelo.has(ruta)) { enVuelo.set( ruta, fetch(`${API}/${ruta}`) .then((res) => { if (!res.ok) throw new Error(`Sectionfy ${res.status}`); return res.json(); }) .catch((error) => { // Sacar la promesa fallida de la cache: un reintento posterior // no debería recibir el mismo rechazo para siempre. enVuelo.delete(ruta); throw error; }), ); }
return enVuelo.get(ruta);}
/** ¿Esta sección se muestra? Es la regla obligatoria del contrato. */const visible = (s) => s.estado.lifecycle === 'active' && s.estado.storefrontVisible;
/** * Secciones de un tipo, filtradas y ordenadas como las dejó el comerciante. * * @param kind - token en minúsculas: 'faq', 'image_carousel', 'specs', … */export function seccionesDeTipo(bundle, kind) { const esperado = kind.toUpperCase();
return Object.entries(bundle.sections) .filter(([key, s]) => // `tipo` es el camino directo; parsear la clave es el respaldo para // respuestas que no lo traigan. s.tipo ? s.tipo === esperado : key.split(`-${bundle.page}-`)[0] === kind, ) .filter(([, s]) => visible(s)) .sort(([, a], [, b]) => a.orden - b.orden) .map(([key, seccion]) => ({ key, ...seccion }));}
/** Escape mínimo para interpolar texto del comerciante en HTML. */export function escapar(valor) { return String(valor ?? '').replace( /[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[c], );}2. La sección
Sección titulada «2. La sección»import { traerBundle, seccionesDeTipo, escapar } from './sectionfy.js';
export async function montarFaq(contenedor) { const { token, page, targetId } = contenedor.dataset;
let bundle; try { bundle = await traerBundle(token, page, targetId); } catch (error) { // Degradar en silencio: es contenido complementario, no la ficha. console.warn('[sectionfy] no se pudo cargar el contenido', error); contenedor.remove(); return; }
const faqs = seccionesDeTipo(bundle, 'faq');
// Sin contenido no dejamos un hueco en la maquetación. if (faqs.length === 0) { contenedor.remove(); return; }
contenedor.innerHTML = faqs .map((faq) => { const items = faq.items .slice() .sort((a, b) => a.orden - b.orden) .map( (item) => ` <details class="sectionfy-faq__item"> <summary>${escapar(item.pregunta)}</summary> <div class="sectionfy-faq__respuesta">${escapar(item.respuesta)}</div> </details> `, ) .join('');
return ` <section class="sectionfy-faq" data-block="${faq.key}"> ${faq.titulo ? `<h2 class="sectionfy-faq__titulo">${escapar(faq.titulo)}</h2>` : ''} ${items} </section> `; }) .join('');}3. El cableado en el tema
Sección titulada «3. El cableado en el tema»Declara el contenedor con los datos que la sección necesita, en el template donde va el contenido:
<div id="sectionfy-faq" data-token="{{ settings.sectionfy_token }}" data-page="product" data-target-id="{{ product.id }}"></div>
<script type="module"> import { montarFaq } from '{{ "js/sectionfy-faq.js" | static_url }}'; const contenedor = document.getElementById('sectionfy-faq'); if (contenedor?.dataset.token) montarFaq(contenedor);</script>El if del final importa: si el comerciante todavía no pegó el token, la sección
no intenta llamar a la API con un valor vacío.
4. Varias secciones, un solo fetch
Sección titulada «4. Varias secciones, un solo fetch»Aquí se paga el diseño del módulo. Tres secciones distintas en la misma página:
<div id="sectionfy-faq" data-token="{{ settings.sectionfy_token }}" data-page="product" data-target-id="{{ product.id }}"></div><div id="sectionfy-specs" data-token="{{ settings.sectionfy_token }}" data-page="product" data-target-id="{{ product.id }}"></div><div id="sectionfy-carrusel" data-token="{{ settings.sectionfy_token }}" data-page="product" data-target-id="{{ product.id }}"></div>
<script type="module"> import { montarFaq } from '{{ "js/sectionfy-faq.js" | static_url }}'; import { montarSpecs } from '{{ "js/sectionfy-specs.js" | static_url }}'; import { montarCarrusel } from '{{ "js/sectionfy-carrusel.js" | static_url }}';
montarFaq(document.getElementById('sectionfy-faq')); montarSpecs(document.getElementById('sectionfy-specs')); montarCarrusel(document.getElementById('sectionfy-carrusel'));</script>Las tres llaman a traerBundle con la misma ruta, así que se dispara una sola
petición. Las otras dos esperan la misma promesa.
5. Respetar el orden del comerciante
Sección titulada «5. Respetar el orden del comerciante»Si quieres que el orden en pantalla sea el que el comerciante armó arrastrando en el panel, no puedes montar cada tipo en un contenedor fijo. Necesitas un único contenedor y un despacho por tipo:
import { traerBundle, escapar } from './sectionfy.js';import { renderFaq } from './render/faq.js';import { renderSpecs } from './render/specs.js';import { renderCarrusel } from './render/carrusel.js';
const RENDERERS = { FAQ: renderFaq, SPECS: renderSpecs, IMAGE_CAROUSEL: renderCarrusel, // Un tipo sin renderer simplemente no se dibuja.};
export async function montarTodo(contenedor) { const { token, page, targetId } = contenedor.dataset; const bundle = await traerBundle(token, page, targetId);
const enOrden = Object.entries(bundle.sections) .filter(([, s]) => s.estado.lifecycle === 'active' && s.estado.storefrontVisible) .sort(([, a], [, b]) => a.orden - b.orden);
for (const [key, seccion] of enOrden) { const render = RENDERERS[seccion.tipo]; if (!render) continue; contenedor.insertAdjacentHTML('beforeend', render({ key, ...seccion }, escapar)); }}Con este patrón, cuando el comerciante reordena en el panel, la tienda refleja el cambio sin tocar el tema.
Checklist
Sección titulada «Checklist»Antes de dar por cerrada la integración:
- El token se lee de un setting del tema, no está hardcodeado.
- Toda interpolación de texto del comerciante pasa por
escapar(). - Se filtra por
estado.lifecycle === 'active' && estado.storefrontVisible. - Se iteran todas las secciones del tipo, no solo la primera.
- Los items se ordenan por
orden. - Se guarda contra
imagen === nullantes de leerimagen.url. - Hay una sola petición por página, no una por sección.
- Un error de la API no rompe la ficha ni deja un esqueleto colgado.
- Sin contenido, el contenedor se saca en vez de quedar vacío.
Siguiente paso
Sección titulada «Siguiente paso»Recetas de fetch para los casos que salen del molde: combinar plantilla e instancia, pedir dos endpoints en paralelo, y precargar un listado.
