Прежде чем писать первый fetch, полезно понять, как Diil смотрит на сайт. Хорошая новость: точно так же, как вы. Сайт — это набор страниц, страница — стопка секций, секция — несколько блоков: тут заголовок, там картинка, внизу список вопросов и ответов. Запомните эти три слова — и весь API уложится в голове.
Модель в одной строке: страница → секция → блок
Всё, что владелец сайта правит в CRM (или прямо на живом сайте в режиме Live), складывается в одно дерево. Ваш сайт читает это дерево через Content API — только чтение, никаких записей — и выводит как угодно: вёрстка, стили и фреймворк целиком ваши.
- Страница — одна страница сайта:
home,pricing,contacts. У неё есть название, адрес (href), SEO-параметры и секции. - Секция — горизонтальный кусок страницы: первый экран, сетка преимуществ, FAQ. Она объединяет блоки, у неё есть флаг
showи позиция (index). - Блок — самая маленькая редактируемая единица: заголовок, картинка, цена, ссылка на кнопке, целый список отзывов. У каждого блока есть
type, и от него зависит, как выглядитcontent.
Блог живёт рядом с этим деревом, а не внутри: у статей свои запросы, свои адреса (slug) и постраничная выдача. Подробности — в разделе Блог.
Разбираем настоящую страницу
Возьмём главную VERTEX — небольшого магазина беспроводных наушников. На глаз там большой первый экран с заголовком и фото товара, ряд преимуществ и 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 из примера выше — всё это маркеры. Их вы подставляете в адрес запроса (/v1/pages/home), и они же служат ключами объектов в ответе.
Правила
- Только латиница, цифры и подчёркивание:
^[A-Za-z0-9_]{2,40}$. - Длина — от 2 до 40 символов.
- Регистр важен:
Heroиhero— два разных маркера. - Всё остальное в адресе получает
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 — всё равно что переименовать колонку в боевой базе.
Почему маркеры, а не id?
Числовой id у каждого объекта тоже есть, смотреть на него никто не запрещает. Но id выдаёт база данных: на разных сайтах и в разных окружениях они разные, а тому, кто будет читать ваш код, не говорят ровным счётом ничего. Маркеры придумывают люди, и читаются они как документация: page.content.hero понятен без комментариев, sections[41] — нет. Пишите шаблоны на маркерах, а id считайте справочной информацией.
Страница common: шапка, подвал и всё общее
Часть контента нужна на каждой странице: логотип, меню, телефон в шапке, ссылки в подвале. Обычно для этого заводят служебную страницу с маркером 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 | Один запрос — все секции и блоки. Выбор по умолчанию. |
| Меню или карта сайта | 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. |