Перш ніж написати перший fetch, варто зрозуміти, як Diil бачить сайт. Гарна новина: так само, як і ви. Сайт — це набір сторінок, сторінка — стос секцій, а секція — жменька блоків: тут заголовок, там зображення, внизу список питань FAQ. Запам’ятайте ці три слова, і весь API вміститься у вас у голові.
Модель в одному рядку: сторінка → секція → блок
Усе, що власник сайту редагує в CRM (або просто на живому сайті в режимі Live), потрапляє в одне дерево. Ваш сайт читає це дерево через Content API, доступний лише для читання, і рендерить його як завгодно — розмітка, стилі й фреймворк на 100% ваші.
- Сторінка — одна сторінка вашого сайту:
home,pricing,contacts. Має назву, шлях (href), SEO-параметри та свої секції. - Секція — горизонтальний зріз сторінки: hero, сітка переваг, FAQ. Групує блоки й має прапорець
showі позицію (index). - Блок — найменша одиниця для редагування: заголовок, зображення, ціна, посилання кнопки, цілий список відгуків. Кожен блок має
type, від якого залежить вигляд йогоcontent.
Блог живе поруч із цим деревом, а не всередині нього: дописи мають власні ендпоінти, slug і пагінацію. Детальніше — у розділі Блог.
Розбираємо справжню сторінку
Погляньмо на головну сторінку VERTEX — невеликого магазину бездротових навушників. Візуально на ній великий hero із заголовком і фото товару, ряд переваг і FAQ унизу. У Diil це виглядає так:
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: [ … ] }А ось що для неї повертає GET /v1/pages/home (секцію features скорочено):
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." }
}
]
}
}
}
}
}
}Прочитайте згори донизу — і закономірність важко не помітити:
contentсторінки — це об’єкт секцій з ключами-маркерами.contentкожної секції — це об’єкт блоків з ключами-маркерами.contentкожного блока — це саме значення, форму якого визначаєtypeблока.
Тож німецька версія першого питання FAQ — це page.content.faq.content.faq_section.content.items[0].question.de. Довго? Так. Несподівано? Ніколи.
Маркери: імена, на які може покластися ваш код
До сторінок, секцій і блоків звертаються за маркерами — короткими машинними іменами, які редактор чи розробник задає в CRM. home, hero і hero_title вище — це все маркери. Саме їх ви підставляєте в URL (/v1/pages/home) і бачите як ключі об’єктів у відповідях.
Правила
- Лише латинські літери, цифри та підкреслення:
^[A-Za-z0-9_]{2,40}$. - Довжина від 2 до 40 символів.
- Регістр важливий:
Heroіhero— два різні маркери. - Будь-що інше в URL отримує
400 {"message":"invalid_marker"}ще до того, як ми почнемо шукати. - Маркери блоків унікальні в межах секції, а не на всьому сайті. Дві секції цілком можуть мати блок
title.
Як називати, щоб потім не шкодувати
- snake_case, малими літерами.
hero_title, а неHeroTitleчиheroTitle2. Регістр має значення, тож один стиль усюди вбереже вас від «чому тут undefined» о другій ночі. - Додавайте до блоків префікс секції.
hero_title,hero_image,faq_section. Голийtitleнормально почувається у відповіді сторінки, але щойно ви запитаєте його окремо через /v1/blocks, він стає неоднозначним — без?sectionперемагає найстаріший збіг. - Називайте зміст, а не вигляд.
promo_bannerпереживе редизайн;red_box_left— ні. - Не перейменовуйте маркери між іншим. Від них залежить ваш код. Перейменувати маркер у CRM — це як перейменувати колонку бази даних на production, тільки в контенті.
Чому маркери, а не id?
Кожен об’єкт має ще й числовий id, і ви можете на нього дивитися. Але id роздає база даних, тож вони різні на різних сайтах і в різних середовищах, і наступній людині, що читатиме ваш код, вони не кажуть нічого. Маркери обирають люди, і вони читаються як документація: page.content.hero пояснює сам себе, sections[41] — ні. Пишіть шаблони на маркерах, а id сприймайте як цікаву дрібницю.
Спільна сторінка: шапка, підвал і компанія
Деякий контент належить кожній сторінці: логотип, меню, телефон у шапці, посилання в підвалі. Звична домовленість — службова сторінка з маркером common, яка тримає ці блоки. Її href дорівнює null — окремо її ніхто не відкриває, — і ви просто запитуєте її поруч із поточною сторінкою:
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; // блоки, спільні для всіх сторінокОбидві відповіді кешуються, тож зайвий запит майже нічого не коштує. Це домовленість, а не магія: якщо ваша команда воліє layout чи shared, API не заперечуватиме.
Мови — це мапи, а не копії
Diil не тримає «англійську сторінку» і «німецьку сторінку» як дві окремі речі. Сторінка одна, а кожне значення, яке можна перекласти, — це мовна мапа:
{ "en": "Earbuds that mute the city", "de": "Ohrhörer, die die Stadt leiser machen" }- Без
?langви отримуєте всі активні мови сайту. З?lang=enчи?lang=en,de— лише їх. - Ключі завжди йдуть у порядку, заданому в CRM, хоч у якому порядку ви їх запитали.
- Запасної мови на сервері немає. Мови без перекладу просто немає в мапі блока, а порожній переклад повертається як
"". Запасний варіант обираєте ви — крихітна функція чекає на сторінці Мови та запасні варіанти. - Сам список мов дає /v1/langs — саме те, що потрібно перемикачу мов.
Типи блоків коротко
type блока каже, чого чекати в його content. Ось шпаргалка; усі типи з повними прикладами — на сторінці Типи блоків.
| Тип | Як виглядає content | Типове застосування |
|---|---|---|
text | { "en": "…", "de": "…" } | Заголовки, короткі тексти |
html | { "en": "<p>…</p>" } — значення є HTML-рядками | Форматований текст |
image, video | { "file": "https://…" } або { "multilang": true, "en": "…", "de": "…" }, коли для кожної мови свій файл | Фото товарів, банери, промовідео |
link | { "url": "…", "target": "_self" | "_blank", "title": { "en": "…" } } | Кнопки, пункти меню |
number, color, boolean | Саме значення або null: 149, "#3D3D5C", true | Ціни, фірмові кольори, перемикачі |
date, date_range | Значення, як його ввели в CRM, або null | Дати розпродажів, події |
object | Поля з ключами-маркерами; кожне поле підкоряється правилам вище | Картка, блок FAQ із заголовком |
array | JSON-масив елементів у формі об’єктів, у порядку з CRM | Списки переваг, відгуки, питання FAQ |
Приховані секції та прапорець show
Редактори можуть вимкнути секцію в CRM — скажімо, сховати FAQ, поки його переписують. Це ставить show: false, але секція все одно повертається, разом з усіма блоками. Що з нею робити, вирішує ваш шаблон:
{page.content.faq?.show && <Faq data={page.content.faq.content} />}Забудете перевірку — і прихована секція радісно з’явиться на живому сайті. API повідомляє, ваш код вирішує.
Порядок і index
Сторінки й секції мають index — свою позицію в CRM, починаючи з 0. Об’єкти у відповіді зазвичай уже йдуть у цьому порядку, але сортування за index — чесний спосіб відтворити те, що бачать редактори, особливо якщо ви рендерите секції динамічно:
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));Елементи всередині блока array не мають index — порядок масиву і є порядком з CRM. Дописи блогу йдуть у порядку сортування, обраному в налаштуваннях блогу в CRM.
Який ендпоінт обрати?
Коротко: запитуйте всю сторінку, якщо немає причин робити інакше. Розгорнуто:
| Вам потрібно… | Використовуйте | Чому |
|---|---|---|
| Усе для рендеру сторінки | GET /v1/pages/:marker | Один запит, усі секції та блоки. Вибір за замовчуванням. |
| Меню чи sitemap | GET /v1/pages | Усі сторінки з href і SEO-параметрами, без контенту. |
| Одна секція — скажімо, промосмуга на кількох сторінках | GET /v1/sections/:marker | Менша відповідь, коли решта сторінки береться деінде. |
| Одне значення — телефон, банер, ціна | GET /v1/blocks/:marker?section=… | Рівно один блок. Передавайте section, щоб бути точними. |
| Список дописів блогу чи один допис | GET /v1/blog, /v1/blog/:slug | Дописи живуть поза деревом сторінок і мають пагінацію. |
| Мови сайту | GET /v1/langs | Перемикачі мов, теги hreflang. |