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 seincontentaussieht.
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:
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"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." }
}
]
}
}
}
}
}
}Lies es von oben nach unten, und das Muster ist kaum zu übersehen:
- Der
contentder Seite ist ein Objekt aus Sections, nach Marker geschlüsselt. - Der
contentjeder Section ist ein Objekt aus Blöcken, nach Marker geschlüsselt. - Der
contentjedes Blocks ist der Wert selbst, geformt durch dentypedes 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:
Heroundherosind 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
titlehaben.
Namenskonventionen, die gut altern
- snake_case, kleingeschrieben.
hero_title, nichtHeroTitleoderheroTitle2. 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 nacktestitleist innerhalb einer Seiten-Response okay, aber sobald du es einzeln über /v1/blocks holst, wird es mehrdeutig — ohne?sectiongewinnt der älteste Treffer. - Benenne die Bedeutung, nicht das Aussehen.
promo_bannerüberlebt ein Redesign;red_box_leftnicht. - 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:
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 teilenBeide 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
?langbekommst du alle aktiven Sprachen der Website. Mit?lang=enoder?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.
| Typ | So sieht content aus | Typischer Einsatz |
|---|---|---|
text | { "en": "…", "de": "…" } | Überschriften, kurze Texte |
html | { "en": "<p>…</p>" } — die Werte sind HTML-Strings | Formatierter Text |
image, video | { "file": "https://…" } oder { "multilang": true, "en": "…", "de": "…" }, wenn jede Sprache ihre eigene Datei hat | Produktfotos, Banner, Promo-Videos |
link | { "url": "…", "target": "_self" | "_blank", "title": { "en": "…" } } | Buttons, Menüpunkte |
number, color, boolean | Der rohe Wert oder null: 149, "#3D3D5C", true | Preise, Markenfarben, Schalter |
date, date_range | Der Wert, wie er im CRM eingegeben wurde, oder null | Aktionszeiträume, Events |
object | Felder nach Marker geschlüsselt; jedes Feld folgt den Regeln oben | Eine Karte, ein FAQ-Block mit Titel |
array | Ein JSON-Array aus Einträgen in Objektform, in CRM-Reihenfolge | Feature-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… | Nimm | Warum |
|---|---|---|
| Alles, um eine Seite zu rendern | GET /v1/pages/:marker | Ein Request, jede Section und jeder Block. Die Standardwahl. |
| Ein Menü oder eine Sitemap | GET /v1/pages | Jede Seite mit href und SEO-Parametern, ohne Content. |
| Eine Section — etwa einen Promo-Streifen, der auf mehreren Seiten vorkommt | GET /v1/sections/:marker | Eine kleinere Response, wenn der Rest der Seite woanders herkommt. |
| Einen einzelnen Wert — eine Telefonnummer, ein Banner, einen Preis | GET /v1/blocks/:marker?section=… | Genau ein Block. Gib section mit, um präzise zu sein. |
| Eine Liste von Blog-Posts oder einen einzelnen Post | GET /v1/blog, /v1/blog/:slug | Posts leben außerhalb des Seitenbaums und kommen mit Pagination. |
| Die Sprachen der Website | GET /v1/langs | Sprachumschalter, hreflang-Tags. |