Diil Docs
  1. Doku
  2. Erste Schritte

So ist Content aufgebaut: Seiten, Abschnitte, Blöcke

Aktualisiert:

Bevor du das erste fetch schreibst, lohnt es sich zu wissen, wie Diil über eine Website denkt. Die gute Nachricht: genau so wie du. Eine Website ist eine Menge von Seiten, eine Seite ist ein Stapel von Sections, und eine Section ist eine Handvoll Blöcke — hier ein Titel, da ein Bild, unten eine Liste mit FAQ-Einträgen. Merk dir diese drei Wörter, und die ganze API passt in deinen Kopf.

Das mentale Modell: Seite → Section → Block

Alles, was der Website-Betreiber im CRM bearbeitet (oder im Live-Modus direkt auf der Live-Website), landet in einem einzigen Baum. Deine Website liest diesen Baum über die schreibgeschützte Content API und rendert ihn, wie sie will — Markup, Styles und Framework bleiben zu 100 % deine Sache.

  • Seite — eine Seite deiner Website: home, pricing, contacts. Sie hat einen Namen, einen Pfad (href), SEO-Parameter und ihre Sections.
  • Section — ein horizontaler Streifen der Seite: der Hero, das Feature-Raster, die FAQ. Sie gruppiert Blöcke und hat ein show-Flag und eine Position (index).
  • Block — die kleinste bearbeitbare Einheit: eine Überschrift, ein Bild, ein Preis, ein Button-Link, eine ganze Liste von Kundenstimmen. Jeder Block hat einen type, der bestimmt, wie sein content aussieht.

Der Blog lebt neben diesem Baum, nicht darin: Posts haben eigene Endpoints, Slugs und Pagination. Mehr dazu unter Blog.

Eine echte Seite, auseinandergenommen

Schauen wir uns die Startseite von VERTEX an, einem kleinen Shop für kabellose Ohrhörer. Optisch gibt es einen großen Hero mit Überschrift und Produktfoto, eine Reihe Features und unten eine FAQ. In Diil sieht das so aus:

Der Baum hinter der VERTEX-Startseitetext
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: [ … ] }

Und das liefert GET /v1/pages/home dafür (die Section features ist gekürzt):

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." }
              }
            ]
          }
        }
      }
    }
  }
}

Lies es von oben nach unten, und das Muster ist kaum zu übersehen:

  • Der content der Seite ist ein Objekt aus Sections, nach Marker geschlüsselt.
  • Der content jeder Section ist ein Objekt aus Blöcken, nach Marker geschlüsselt.
  • Der content jedes Blocks ist der Wert selbst, geformt durch den type des Blocks.

Die deutsche Version der ersten FAQ-Frage ist also page.content.faq.content.faq_section.content.items[0].question.de. Lang? Ja. Überraschend? Nie.

Marker: Namen, auf die sich dein Code verlassen kann

Seiten, Sections und Blöcke werden über Marker angesprochen — kurze maschinenlesbare Namen, die jemand aus Redaktion oder Entwicklung im CRM festlegt. home, hero und hero_title oben sind allesamt Marker. Sie stehen in URLs (/v1/pages/home) und tauchen in Responses als Objekt-Keys auf.

Die Regeln

  • Nur lateinische Buchstaben, Ziffern und Unterstriche: ^[A-Za-z0-9_]{2,40}$.
  • Zwischen 2 und 40 Zeichen lang.
  • Groß- und Kleinschreibung zählt: Hero und hero sind zwei verschiedene Marker.
  • Alles andere in einer URL bekommt 400 {"message":"invalid_marker"}, bevor wir überhaupt anfangen zu suchen.
  • Block-Marker sind innerhalb einer Section eindeutig, nicht auf der ganzen Website. Zwei Sections können beide einen Block title haben.

Namenskonventionen, die gut altern

  • snake_case, kleingeschrieben. hero_title, nicht HeroTitle oder heroTitle2. Weil Groß- und Kleinschreibung zählt, erspart dir ein einheitlicher Stil das „Warum ist das undefined?“ um 2 Uhr nachts.
  • Blöcke mit ihrer Section prefixen. hero_title, hero_image, faq_section. Ein nacktes title ist innerhalb einer Seiten-Response okay, aber sobald du es einzeln über /v1/blocks holst, wird es mehrdeutig — ohne ?section gewinnt der älteste Treffer.
  • Benenne die Bedeutung, nicht das Aussehen. promo_banner überlebt ein Redesign; red_box_left nicht.
  • Marker nicht leichtfertig umbenennen. Dein Code hängt an ihnen. Einen Marker im CRM umzubenennen ist die Content-Version davon, in Produktion eine Datenbankspalte umzubenennen.

Warum Marker und nicht IDs?

Jedes Objekt hat auch eine numerische id, und du darfst sie dir gern ansehen. Aber IDs vergibt die Datenbank, sie unterscheiden sich also zwischen Websites und Umgebungen, und der nächsten Person, die deinen Code liest, sagen sie überhaupt nichts. Marker werden von Menschen gewählt und lesen sich wie Dokumentation: page.content.hero erklärt sich selbst, sections[41] nicht. Schreib deine Templates gegen Marker und behandle IDs als Trivia.

Die gemeinsame Seite: Header, Footer und Co.

Mancher Content gehört auf jede Seite: das Logo, das Menü, die Telefonnummer im Header, die Footer-Links. Üblich ist eine Service-Seite mit dem Marker common, die diese Blöcke enthält. Ihr href ist null — niemand öffnet sie für sich allein —, und du holst sie einfach neben der aktuellen Seite:

Layout-Daten: aktuelle Seite + 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; // Blöcke, die alle Seiten teilen

Beide Responses werden gecacht, der zusätzliche Request ist also so gut wie gratis. Das ist eine Konvention, keine Magie: Wenn dein Team lieber layout oder shared nimmt, hat die API nichts dagegen.

Sprachen sind Maps, keine Kopien

Diil führt „die englische Seite“ und „die deutsche Seite“ nicht als zwei getrennte Dinge. Es gibt eine Seite, und jeder übersetzbare Wert ist eine Sprach-Map:

{ "en": "Earbuds that mute the city", "de": "Ohrhörer, die die Stadt leiser machen" }
  • Ohne ?lang bekommst du alle aktiven Sprachen der Website. Mit ?lang=en oder ?lang=en,de — nur diese.
  • Die Keys kommen immer in der im CRM festgelegten Reihenfolge, egal in welcher Reihenfolge du gefragt hast.
  • Es gibt keinen Fallback auf dem Server. Eine Sprache ohne Übersetzung fehlt in der Map eines Blocks einfach, und eine leere Übersetzung kommt als "" zurück. Den Fallback wählst du selbst — ein winziger Helper wartet unter Sprachen & Fallbacks.
  • Die Liste der Sprachen selbst kommt von /v1/langs — genau das, was ein Sprachumschalter braucht.

Blocktypen auf einen Blick

Der type eines Blocks sagt dir, was dich in seinem content erwartet. Hier der Spickzettel; jeden Typ mit vollständigen Beispielen findest du unter Blocktypen.

TypSo sieht content ausTypischer Einsatz
text{ "en": "…", "de": "…" }Überschriften, kurze Texte
html{ "en": "<p>…</p>" } — die Werte sind HTML-StringsFormatierter Text
image, video{ "file": "https://…" } oder { "multilang": true, "en": "…", "de": "…" }, wenn jede Sprache ihre eigene Datei hatProduktfotos, Banner, Promo-Videos
link{ "url": "…", "target": "_self" | "_blank", "title": { "en": "…" } }Buttons, Menüpunkte
number, color, booleanDer rohe Wert oder null: 149, "#3D3D5C", truePreise, Markenfarben, Schalter
date, date_rangeDer Wert, wie er im CRM eingegeben wurde, oder nullAktionszeiträume, Events
objectFelder nach Marker geschlüsselt; jedes Feld folgt den Regeln obenEine Karte, ein FAQ-Block mit Titel
arrayEin JSON-Array aus Einträgen in Objektform, in CRM-ReihenfolgeFeature-Listen, Kundenstimmen, FAQ-Einträge

Versteckte Sections und das show-Flag

Die Redaktion kann eine Section im CRM ausschalten — etwa die FAQ verstecken, während sie umgeschrieben wird. Das setzt show: false, aber die Section wird trotzdem ausgeliefert, samt aller Blöcke. Was damit passiert, entscheidet dein Template:

{page.content.faq?.show && <Faq data={page.content.faq.content} />}

Vergiss die Prüfung, und eine versteckte Section taucht fröhlich auf der Live-Website auf. Die API berichtet; dein Code entscheidet.

Sortieren mit index

Seiten und Sections tragen einen index — ihre Position im CRM, beginnend bei 0. Die Objekte in der Response kommen meist schon in dieser Reihenfolge, aber nach index zu sortieren ist der ehrliche Weg, das nachzubauen, was die Redaktion sieht — besonders wenn du Sections dynamisch renderst:

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));

Einträge in einem array-Block haben keinen index — die Reihenfolge im Array ist die CRM-Reihenfolge. Blog-Posts folgen der Sortierung, die in den Blog-Einstellungen im CRM gewählt ist.

Welchen Endpoint soll ich nehmen?

Kurzfassung: Hol die ganze Seite, außer du hast einen Grund dagegen. Langfassung:

Du brauchst…NimmWarum
Alles, um eine Seite zu rendernGET /v1/pages/:markerEin Request, jede Section und jeder Block. Die Standardwahl.
Ein Menü oder eine SitemapGET /v1/pagesJede Seite mit href und SEO-Parametern, ohne Content.
Eine Section — etwa einen Promo-Streifen, der auf mehreren Seiten vorkommtGET /v1/sections/:markerEine kleinere Response, wenn der Rest der Seite woanders herkommt.
Einen einzelnen Wert — eine Telefonnummer, ein Banner, einen PreisGET /v1/blocks/:marker?section=…Genau ein Block. Gib section mit, um präzise zu sein.
Eine Liste von Blog-Posts oder einen einzelnen PostGET /v1/blog, /v1/blog/:slugPosts leben außerhalb des Seitenbaums und kommen mit Pagination.
Die Sprachen der WebsiteGET /v1/langsSprachumschalter, hreflang-Tags.

Wie geht's weiter