Antes de escribir un solo fetch, merece la pena saber cómo ve Diil una web. La buena noticia: la ve igual que tú. Un sitio es un conjunto de páginas, una página es una pila de secciones y una sección es un puñado de bloques: un título aquí, una imagen allá, una lista de preguntas frecuentes al final. Aprende estas tres palabras y te cabrá toda la API en la cabeza.
El modelo mental: página → sección → bloque
Todo lo que el dueño del sitio edita en el CRM (o directamente en la web en vivo, en modo Live) acaba en un único árbol. Tu web lee ese árbol a través de la Content API de solo lectura y lo pinta como quiera: el marcado, los estilos y el framework siguen siendo 100% tuyos.
- Página: una página de tu web:
home,pricing,contacts. Tiene un nombre, una ruta (href), parámetros SEO y sus secciones. - Sección: una franja horizontal de la página: el hero, la rejilla de características, las FAQ. Agrupa bloques y tiene un indicador
showy una posición (index). - Bloque: lo más pequeño que se puede editar: un titular, una imagen, un precio, el enlace de un botón, una lista entera de testimonios. Cada bloque tiene un
typeque decide qué forma tiene sucontent.
El blog vive junto a este árbol, no dentro: los posts tienen sus propios endpoints, slugs y paginación. Más sobre eso en Blog.
Una página real, desmontada
Veamos la página de inicio de VERTEX, una pequeña tienda de auriculares inalámbricos. A la vista tiene un gran hero con un titular y una foto del producto, una fila de características y unas FAQ al final. En Diil se ve así:
page home
├── section hero index 0, show: true
│ ├── block hero_title text "Earbuds that mute the city"
│ └── block hero_image image hero.jpg
├── section features index 1, show: true
│ └── block features_list array [ {…}, {…}, {…} ]
└── section faq index 2, show: true
└── block faq_section object { title, items: [ … ] }Y esto es lo que devuelve GET /v1/pages/home para ella (la sección features está recortada):
curl "https://back.sitecog.com/content/v1/pages/home?lang=en,de" \
-H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en,de', {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
});
const page = await res.json();
const { hero, faq } = page.content;
console.log(hero.content.hero_title.content.de); // "Ohrhörer, die die Stadt leiser machen"
console.log(faq.content.faq_section.content.items.length); // 2{
"id": 26,
"marker": "home",
"name": "Home",
"href": "/",
"index": 0,
"params": {
"title": { "en": "VERTEX Air 3 — wireless earbuds", "de": "VERTEX Air 3 — kabellose Ohrhörer" }
},
"content": {
"hero": {
"id": 41,
"marker": "hero",
"name": "Hero",
"index": 0,
"show": true,
"content": {
"hero_title": {
"id": 95,
"marker": "hero_title",
"name": "Hero title",
"type": "text",
"multilang": true,
"updatedAt": "2026-09-20T16:33:23.000Z",
"content": {
"en": "Earbuds that mute the city",
"de": "Ohrhörer, die die Stadt leiser machen"
}
},
"hero_image": {
"id": 96,
"marker": "hero_image",
"name": "Hero image",
"type": "image",
"multilang": false,
"updatedAt": "2026-09-18T09:12:40.000Z",
"content": { "file": "https://cdn.example.com/storage/your-site/hero.jpg" }
}
}
},
"features": {
"id": 42,
"marker": "features",
"name": "Features",
"index": 1,
"show": true,
"content": { "…": "…" }
},
"faq": {
"id": 44,
"marker": "faq",
"name": "FAQ",
"index": 2,
"show": true,
"content": {
"faq_section": {
"id": 102,
"marker": "faq_section",
"name": "FAQ",
"type": "object",
"multilang": false,
"updatedAt": "2026-09-22T11:05:10.000Z",
"content": {
"title": { "en": "FAQ", "de": "Häufige Fragen" },
"items": [
{
"question": { "en": "How long is delivery?", "de": "Wie lange dauert der Versand?" },
"answer": { "en": "1–3 days.", "de": "1–3 Tage." }
},
{
"question": { "en": "Do they work with iPhone?", "de": "Funktionieren sie mit dem iPhone?" },
"answer": { "en": "Yes, and with Android too.", "de": "Ja, und auch mit Android." }
}
]
}
}
}
}
}
}Léelo de arriba abajo y el patrón salta a la vista:
- El
contentde la página es un objeto de secciones con su marcador como clave. - El
contentde cada sección es un objeto de bloques con su marcador como clave. - El
contentde cada bloque es el valor en sí, con la forma que marca eltypedel bloque.
Así que la versión alemana de la primera pregunta de las FAQ es page.content.faq.content.faq_section.content.items[0].question.de. ¿Largo? Sí. ¿Sorprendente? Nunca.
Marcadores: nombres en los que tu código puede confiar
Las páginas, secciones y bloques se identifican por marcadores: nombres cortos para máquinas que un editor o un desarrollador define en el CRM. home, hero y hero_title de arriba son marcadores. Son lo que pones en las URL (/v1/pages/home) y lo que ves como claves de los objetos en las respuestas.
Las reglas
- Solo letras latinas, dígitos y guiones bajos:
^[A-Za-z0-9_]{2,40}$. - De 2 a 40 caracteres.
- Distinguen mayúsculas y minúsculas:
Heroyheroson dos marcadores distintos. - Cualquier otra cosa en una URL recibe
400 {"message":"invalid_marker"}antes incluso de que empecemos a buscar. - Los marcadores de bloque son únicos dentro de una sección, no en todo el sitio. Dos secciones pueden tener cada una su bloque
title.
Convenciones de nombres que envejecen bien
- snake_case, en minúsculas.
hero_title, noHeroTitleniheroTitle2. Como las mayúsculas importan, un único estilo en todas partes te ahorra el “¿por qué esto es undefined?” a las dos de la madrugada. - Prefija los bloques con su sección.
hero_title,hero_image,faq_section. Untitlea secas va bien dentro de la respuesta de una página, pero en cuanto lo pides por separado a través de /v1/blocks se vuelve ambiguo: sin?sectiongana la coincidencia más antigua. - Nombra el significado, no el aspecto.
promo_bannersobrevive a un rediseño;red_box_left, no. - No renombres marcadores a la ligera. Tu código depende de ellos. Renombrar uno en el CRM es el equivalente, en contenido, a renombrar una columna de la base de datos en producción.
¿Por qué marcadores y no ids?
Cada objeto también tiene un id numérico, y puedes mirarlo cuanto quieras. Pero los ids los reparte la base de datos, así que cambian entre sitios y entornos, y a la siguiente persona que lea tu código no le dicen absolutamente nada. Los marcadores los eligen personas y se leen como documentación: page.content.hero se explica solo; sections[41], no. Escribe tus plantillas contra marcadores y trata los ids como una curiosidad.
La página común: cabecera, pie y compañía
Hay contenido que pertenece a todas las páginas: el logo, el menú, el teléfono de la cabecera, los enlaces del pie. La convención habitual es una página de servicio con el marcador common que guarda esos bloques. Su href es null (nadie la abre por sí sola) y simplemente la pides junto a la página actual:
const headers = { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' };
const [page, common] = await Promise.all([
fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', { headers }).then((r) => r.json()),
fetch('https://back.sitecog.com/content/v1/pages/common?lang=en', { headers }).then((r) => r.json()),
]);
const footer = common.content.footer.content; // bloques compartidos por todas las páginasLas dos respuestas se cachean, así que la petición extra sale casi gratis. Es una convención, no magia: si tu equipo prefiere layout o shared, a la API le da igual.
Los idiomas son mapas, no copias
Diil no guarda “la página en inglés” y “la página en alemán” como dos cosas separadas. Hay una sola página, y cada valor traducible es un mapa de idiomas:
{ "en": "Earbuds that mute the city", "de": "Ohrhörer, die die Stadt leiser machen" }- Sin
?langrecibes todos los idiomas activos del sitio. Con?lang=eno?lang=en,de, solo esos. - Las claves siempre llegan en el orden fijado en el CRM, da igual en qué orden las pidas.
- No hay respaldo en el servidor. Un idioma sin traducción simplemente no aparece en el mapa de un bloque, y una traducción vacía vuelve como
"". Elegir el respaldo es cosa tuya: tienes un helper diminuto esperándote en Idiomas y respaldos. - La lista de idiomas en sí sale de /v1/langs: justo lo que necesita un selector de idioma.
Tipos de bloque de un vistazo
El type de un bloque te dice qué esperar en su content. Aquí tienes la chuleta; cada tipo con ejemplos completos está en Tipos de bloque.
| Tipo | Qué forma tiene content | Uso típico |
|---|---|---|
text | { "en": "…", "de": "…" } | Titulares, textos cortos |
html | { "en": "<p>…</p>" }: los valores son cadenas HTML | Texto con formato |
image, video | { "file": "https://…" }, o { "multilang": true, "en": "…", "de": "…" } cuando cada idioma tiene su propio archivo | Fotos de producto, banners, vídeos promocionales |
link | { "url": "…", "target": "_self" | "_blank", "title": { "en": "…" } } | Botones, elementos de menú |
number, color, boolean | El valor tal cual o null: 149, "#3D3D5C", true | Precios, colores de marca, interruptores |
date, date_range | El valor tal como se introdujo en el CRM, o null | Fechas de rebajas, eventos |
object | Campos con su marcador como clave; cada campo sigue las reglas de arriba | Una tarjeta, un bloque de FAQ con título |
array | Un array JSON de elementos con forma de objeto, en el orden del CRM | Listas de características, testimonios, preguntas de FAQ |
Secciones ocultas y el indicador show
Los editores pueden apagar una sección en el CRM; por ejemplo, ocultar las FAQ mientras se reescriben. Eso pone show: false, pero la sección se sigue devolviendo, con bloques y todo. Qué hacer con ella lo decide tu plantilla:
{page.content.faq?.show && <Faq data={page.content.faq.content} />}Olvídate de la comprobación y una sección oculta aparecerá tan campante en la web en vivo. La API informa; tu código decide.
Ordenar con index
Las páginas y las secciones llevan un index: su posición en el CRM, empezando por 0. Los objetos de la respuesta suelen llegar ya en ese orden, pero ordenar por index es la forma honesta de reconstruir lo que ven los editores, sobre todo si pintas las secciones dinámicamente:
const sections = Object.values(page.content)
.filter((section) => section.show)
.sort((a, b) => a.index - b.index);
sections.forEach((section) => render(section.marker, section.content));Los elementos dentro de un bloque array no tienen index: el orden del array es el orden del CRM. Los posts del blog siguen el orden elegido en los ajustes del blog en el CRM.
¿Qué endpoint uso?
Versión corta: pide la página entera salvo que tengas un motivo para no hacerlo. Versión larga:
| Necesitas… | Usa | Por qué |
|---|---|---|
| Todo para pintar una página | GET /v1/pages/:marker | Una petición, todas las secciones y bloques. La opción por defecto. |
| Un menú o un sitemap | GET /v1/pages | Todas las páginas con href y parámetros SEO, sin contenido. |
| Una sección; por ejemplo, una franja promocional reutilizada en varias páginas | GET /v1/sections/:marker | Una respuesta más pequeña cuando el resto de la página sale de otro sitio. |
| Un valor: un teléfono, un banner, un precio | GET /v1/blocks/:marker?section=… | Exactamente un bloque. Pasa section para ser preciso. |
| Una lista de posts del blog o un solo post | GET /v1/blog, /v1/blog/:slug | Los posts viven fuera del árbol de páginas y vienen con paginación. |
| Los idiomas del sitio | GET /v1/langs | Selectores de idioma, etiquetas hreflang. |