Before you write a single fetch, it pays to know how Diil thinks about a website. The good news: it thinks the way you already do. A site is a set of pages, a page is a stack of sections, and a section is a handful of blocks — a title here, an image there, a list of FAQ items at the bottom. Learn these three words and the whole API fits in your head.
The mental model: page → section → block
Everything the site owner edits in the CRM (or right on the live site in Live mode) ends up in one tree. Your site reads that tree through the read-only Content API and renders it however it likes — markup, styles and framework stay 100% yours.
- Page — one page of your site:
home,pricing,contacts. It has a name, a path (href), SEO params and its sections. - Section — a horizontal slice of the page: the hero, the feature grid, the FAQ. It groups blocks and has a
showflag and a position (index). - Block — the smallest editable thing: a headline, an image, a price, a button link, a whole list of testimonials. Every block has a
typethat decides what itscontentlooks like.
The blog lives next to this tree, not inside it: posts have their own endpoints, slugs and pagination. More on that in Blog.
A real page, taken apart
Let's look at the home page of VERTEX, a small shop that sells wireless earbuds. Visually it has a big hero with a headline and a product shot, a row of features, and an FAQ at the bottom. In Diil it looks like this:
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: [ … ] }And this is what GET /v1/pages/home returns for it (the features section is trimmed):
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." }
}
]
}
}
}
}
}
}Read it top to bottom and the pattern is hard to miss:
- The page's
contentis an object of sections keyed by marker. - Each section's
contentis an object of blocks keyed by marker. - Each block's
contentis the value itself, shaped by the block'stype.
So the German version of the first FAQ question is page.content.faq.content.faq_section.content.items[0].question.de. Long? Yes. Surprising? Never.
Markers: names your code can rely on
Pages, sections and blocks are addressed by markers — short machine names that an editor or developer sets in the CRM. home, hero and hero_title above are all markers. They are what you put in URLs (/v1/pages/home) and what you see as object keys in responses.
The rules
- Latin letters, digits and underscores only:
^[A-Za-z0-9_]{2,40}$. - From 2 to 40 characters long.
- Case-sensitive:
Heroandheroare two different markers. - Anything else in a URL gets
400 {"message":"invalid_marker"}before we even start looking. - Block markers are unique within a section, not across the whole site. Two sections can both have a
titleblock.
Naming conventions that age well
- snake_case, lowercase.
hero_title, notHeroTitleorheroTitle2. Since case matters, one style everywhere saves you from “why is this undefined” at 2 a.m. - Prefix blocks with their section.
hero_title,hero_image,faq_section. A baretitleis fine inside a page response, but the moment you fetch it on its own through /v1/blocks it becomes ambiguous — without?sectionthe oldest match wins. - Name the meaning, not the look.
promo_bannersurvives a redesign;red_box_leftdoes not. - Don't rename markers casually. Your code depends on them. Renaming one in the CRM is the content version of renaming a database column in production.
Why markers and not ids?
Every object also has a numeric id, and you are welcome to look at it. But ids are handed out by the database, so they differ between sites and environments, and they tell the next person reading your code nothing at all. Markers are chosen by humans and read like documentation: page.content.hero explains itself, sections[41] does not. Write your templates against markers and treat ids as trivia.
The common page: header, footer and friends
Some content belongs to every page: the logo, the menu, the phone number in the header, the footer links. The usual convention is a service page with the marker common that holds those blocks. Its href is null — nobody opens it on its own — and you simply fetch it next to the current page:
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; // blocks shared by every pageBoth responses are cached, so the extra request is close to free. It is a convention, not magic: if your team prefers layout or shared, the API won't mind.
Languages are maps, not copies
Diil does not keep “the English page” and “the German page” as two separate things. There is one page, and every translatable value is a language map:
{ "en": "Earbuds that mute the city", "de": "Ohrhörer, die die Stadt leiser machen" }- Without
?langyou get every active language of the site. With?lang=enor?lang=en,de— only those. - Keys always come in the order set in the CRM, whatever order you asked for.
- There is no fallback on the server. A language with no translation is simply missing from a block's map, and an empty translation comes back as
"". Choosing a fallback is up to you — a tiny helper is waiting on Languages & fallbacks. - The list of languages itself comes from /v1/langs — exactly what a language switcher needs.
Block types at a glance
A block's type tells you what to expect in its content. Here is the cheat sheet; every type with full examples lives on Block types.
| Type | What content looks like | Typical use |
|---|---|---|
text | { "en": "…", "de": "…" } | Headlines, short copy |
html | { "en": "<p>…</p>" } — values are HTML strings | Formatted text |
image, video | { "file": "https://…" }, or { "multilang": true, "en": "…", "de": "…" } when each language has its own file | Product shots, banners, promo videos |
link | { "url": "…", "target": "_self" | "_blank", "title": { "en": "…" } } | Buttons, menu items |
number, color, boolean | The raw value or null: 149, "#3D3D5C", true | Prices, brand colours, toggles |
date, date_range | The value as entered in the CRM, or null | Sale dates, events |
object | Fields keyed by marker; each field follows the rules above | A card, an FAQ block with a title |
array | A JSON array of items shaped like objects, in CRM order | Feature lists, testimonials, FAQ items |
Hidden sections and the show flag
Editors can switch a section off in the CRM — say, hide the FAQ while it is being rewritten. That sets show: false, but the section is still returned, blocks and all. What to do with it is your template's call:
{page.content.faq?.show && <Faq data={page.content.faq.content} />}Forget the check and a hidden section will happily show up on the live site. The API reports; your code decides.
Ordering with index
Pages and sections carry an index — their position in the CRM, starting at 0. Response objects usually arrive in that order already, but sorting by index is the honest way to rebuild what editors see, especially if you render sections dynamically:
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));Items inside an array block have no index — the array order is the CRM order. Blog posts follow the sort order chosen in the blog settings in the CRM.
Which endpoint should I use?
Short version: fetch the whole page unless you have a reason not to. Long version:
| You need… | Use | Why |
|---|---|---|
| Everything to render a page | GET /v1/pages/:marker | One request, every section and block. The default choice. |
| A menu or a sitemap | GET /v1/pages | Every page with href and SEO params, without content. |
| One section — say, a promo strip reused on several pages | GET /v1/sections/:marker | A smaller response when the rest of the page comes from elsewhere. |
| One value — a phone number, a banner, a price | GET /v1/blocks/:marker?section=… | Exactly one block. Pass section to be precise. |
| A list of blog posts or a single post | GET /v1/blog, /v1/blog/:slug | Posts live outside the page tree and come with pagination. |
| The site's languages | GET /v1/langs | Language switchers, hreflang tags. |