Ir al contenido

Recetas de fetch

Patrones concretos para los casos que salen del molde. Todos asumen el módulo sectionfy.js de la guía de sección custom.

El patrón base, y el que más impacto tiene: traerBundle cachea la promesa, no el resultado.

La diferencia no es un detalle de estilo. Si cacheas el resultado, queda una ventana abierta entre que arranca el fetch y que termina: durante esos milisegundos la cache sigue vacía, así que la segunda y la tercera sección disparan sus propios requests. Cacheando la promesa, las tres esperan la misma.

La implementación está en el módulo cliente. Todo lo que sigue la asume.

Tanto una ficha de producto como una página de categoría tienen dos niveles de contenido:

PáginaLo específico de ese recursoLo común a todas
Producto/{token}/product/{id}/{token}/product-page
Categoría/{token}/category/{id}/{token}/category-page

Si tu tema usa los dos, pídelos en paralelo y únelos:

export async function contenidoDeFicha(token, productId) {
const [instancia, plantilla] = await Promise.all([
traerBundle(token, 'product', productId),
traerBundle(token, 'product-page'),
]);
// Las claves nunca chocan: la instancia lleva el productId donde la
// plantilla lleva el literal `general`.
return {
page: 'product',
product_id: productId,
sections: { ...plantilla.sections, ...instancia.sections },
};
}

La categoría es la misma función con otros dos argumentos:

export async function contenidoDeCategoria(token, categoryId) {
const [instancia, plantilla] = await Promise.all([
traerBundle(token, 'category', categoryId),
traerBundle(token, 'category-page'),
]);
return {
page: 'category',
category_id: categoryId,
sections: { ...plantilla.sections, ...instancia.sections },
};
}

Recuerda que los dos responden "page": "product". No uses ese campo para saber cuál es cuál.

key_features_banner vive en su propio endpoint. Si la página necesita las dos cosas, no las encadenes:

const API = 'https://sectionfy.codefy-apps.com/api/public';
export async function todoElContenido(token, productId) {
const [bundle, banners] = await Promise.all([
fetch(`${API}/content/${token}/product/${productId}`).then((r) => r.json()),
fetch(`${API}/key-features-banner/${token}`).then((r) => r.json()),
]);
return { bundle, banners };
}

Si quieres que un fallo del banner no eche abajo la ficha, usa Promise.allSettled:

const [bundleRes, bannersRes] = await Promise.allSettled([...]);
const bundle = bundleRes.status === 'fulfilled' ? bundleRes.value : { sections: {} };
const banners = bannersRes.status === 'fulfilled' ? bannersRes.value : { sections: {} };

Devolver { sections: {} } como respaldo deja que el resto del código siga sin ramas especiales.

En una página de categoría o en un buscador, products/all te trae todos los productos con contenido en una llamada, en vez de una por tarjeta:

export async function indiceDeProductos(token) {
const res = await fetch(`${API}/content/${token}/products/all`);
if (!res.ok) throw new Error(`Sectionfy ${res.status}`);
const { products } = await res.json();
// `products` es un ARRAY. Indexarlo hace las búsquedas O(1).
return new Map(products.map((p) => [p.product_id, p.sections]));
}
const indice = await indiceDeProductos(TOKEN);
const secciones = indice.get('333333174') ?? {};

Ten en cuenta el tamaño: no pagina, así que en un catálogo grande el payload puede ser pesado. Vale la pena si necesitas varios productos; para uno solo, pide /{token}/product/{id}.

La home no lleva targetId:

const bundle = await traerBundle(token, 'home-page');
// bundle.page === 'home' (normalizado, sin el sufijo)

De punta a punta, en una plantilla de categoría. El categoryId lo da tu tema —en Tiendanube, category.id— y las secciones se filtran con los mismos dos helpers compartidos que usa cualquier otra página:

const bundle = await traerBundle(token, 'category', categoryId);
// seccionesDeTipo ya aplica la regla de render y ordena por `orden`.
for (const seccion of seccionesDeTipo(bundle, 'FAQ')) {
const html = seccion.items
.map((item) => `<dt>${escapar(item.pregunta)}</dt><dd>${escapar(item.respuesta)}</dd>`)
.join('');
document.querySelector('#ndp-faqs').innerHTML = `<dl>${html}</dl>`;
}

Una categoría sin contenido configurado responde 200 con sections: {}, no 404 — así que el bucle simplemente no itera y no necesitas una rama para el caso vacío.

Los tipos mínimos que hacen falta para trabajar cómodo:

type Imagen = { url: string; alt: string | null };
type Estado = {
lifecycle: 'active' | 'archived' | string;
storefrontVisible: boolean;
};
type Seccion = {
tipo?: string;
titulo: string | null;
nombre_personalizado?: string;
texto?: string | null;
imagen?: Imagen | null;
items: unknown[];
estado: Estado;
orden: number;
indice?: number;
};
type Bundle = {
page: 'home' | 'category' | 'product';
product_id?: string;
category_id?: string;
sections: Record<string, Seccion>;
};

items va como unknown[] a propósito: su shape depende del tipo de sección. Restríngelo cuando ya sabes con qué tipo estás trabajando:

type FaqItem = { pregunta: string; respuesta: string; orden: number };
const faqs = seccionesDeTipo(bundle, 'faq') as Array<Seccion & { items: FaqItem[] }>;

Si prefieres tipos generados en vez de escritos a mano, el contrato OpenAPI sirve de entrada para cualquier generador.

Antes de conectar nada, mira qué devuelve realmente el token con el que estás trabajando: el probador muestra la respuesta y los headers de cache.