Diil Docs
  1. Documentación
  2. Primeros pasos

Cómo se organiza el contenido: páginas, secciones, bloques

Actualizado:

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 show y 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 type que decide qué forma tiene su content.

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í:

El árbol detrás de la página de inicio de VERTEXtext
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"
200 OKjson
{
  "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 content de la página es un objeto de secciones con su marcador como clave.
  • El content de cada sección es un objeto de bloques con su marcador como clave.
  • El content de cada bloque es el valor en sí, con la forma que marca el type del 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: Hero y hero son 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, no HeroTitle ni heroTitle2. 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. Un title a 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 ?section gana la coincidencia más antigua.
  • Nombra el significado, no el aspecto. promo_banner sobrevive 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:

Datos del layout: página actual + commonjs
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áginas

Las 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 ?lang recibes todos los idiomas activos del sitio. Con ?lang=en o ?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.

TipoQué forma tiene contentUso típico
text{ "en": "…", "de": "…" }Titulares, textos cortos
html{ "en": "<p>…</p>" }: los valores son cadenas HTMLTexto con formato
image, video{ "file": "https://…" }, o { "multilang": true, "en": "…", "de": "…" } cuando cada idioma tiene su propio archivoFotos de producto, banners, vídeos promocionales
link{ "url": "…", "target": "_self" | "_blank", "title": { "en": "…" } }Botones, elementos de menú
number, color, booleanEl valor tal cual o null: 149, "#3D3D5C", truePrecios, colores de marca, interruptores
date, date_rangeEl valor tal como se introdujo en el CRM, o nullFechas de rebajas, eventos
objectCampos con su marcador como clave; cada campo sigue las reglas de arribaUna tarjeta, un bloque de FAQ con título
arrayUn array JSON de elementos con forma de objeto, en el orden del CRMListas 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…UsaPor qué
Todo para pintar una páginaGET /v1/pages/:markerUna petición, todas las secciones y bloques. La opción por defecto.
Un menú o un sitemapGET /v1/pagesTodas las páginas con href y parámetros SEO, sin contenido.
Una sección; por ejemplo, una franja promocional reutilizada en varias páginasGET /v1/sections/:markerUna respuesta más pequeña cuando el resto de la página sale de otro sitio.
Un valor: un teléfono, un banner, un precioGET /v1/blocks/:marker?section=…Exactamente un bloque. Pasa section para ser preciso.
Una lista de posts del blog o un solo postGET /v1/blog, /v1/blog/:slugLos posts viven fuera del árbol de páginas y vienen con paginación.
Los idiomas del sitioGET /v1/langsSelectores de idioma, etiquetas hreflang.

Y ahora, ¿qué?