Diil Docs
  1. Документація
  2. Довідник API

GET /v1/blog — статті, сторінки списку, одна стаття

Оновлено:

Редактори пишуть дописи в CRM, ваш сайт їх показує. API блогу дає список зі сторінками для головної блогу і повний допис за його slug для сторінки статті — два ендпоінти, і прохання «а опублікуйте це до п’ятниці» більше не падають вам у пошту.

Два ендпоінти — по одному на кожен тип сторінки, яку ви збудуєте:

  • GET /v1/blog — список: заголовки, обкладинки, автори й дати, сторінка за сторінкою. Без текстів дописів, тож він легкий.
  • GET /v1/blog/:slug — один допис з усім вмістом, включно з HTML-текстом.

Список дописів блогу

GET/v1/blog

https://back.sitecog.com/content/v1/blog

Повертає вікно видимих дописів і все, що потрібно для пагінації: загальну кількість, кількість сторінок, чи є ще дописи і звідки починається наступне вікно.

Параметри запиту

langquerystringнеобовʼязковоЗа замовчуванням: усі активні мови
Які переклади включити в title та image. Один код, список через кому (en,de) або повторений параметр. Див. Мови.
limitquerynumberнеобовʼязковоЗа замовчуванням: розмір сторінки блогу з CRM, інакше 12
Скільки дописів повернути, від 1 до 50. Якщо редактор задав розмір сторінки в налаштуваннях блогу в CRM, за замовчуванням буде він; якщо ні — 12.
offsetquerynumberнеобовʼязковоЗа замовчуванням: 0
Скільки дописів пропустити, від 0 до 100000. У парі з limit — для нескінченної прокрутки й кнопок «Показати ще».
pagequerynumberнеобовʼязково
Номер сторінки, починаючи з 1. Альтернатива offset для нумерованої пагінації. Якщо передано обидва, перемагає page.
perPagequerynumberнеобовʼязковоЗа замовчуванням: як limit
Дописів на сторінку, з тими самими межами, що й limit (1–50). Якщо передано обидва, перемагає perPage.
x-crm-keyheaderstringобовʼязково
Ключ вашого сайту. Можна передати і як ?key=. Див. Ключі сайту.

Приклад запиту

curl "https://back.sitecog.com/content/v1/blog?lang=en&limit=2" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"

Приклад відповіді

200 OKjson
{
  "total": 29,
  "limit": 2,
  "offset": 0,
  "page": 1,
  "pages": 15,
  "hasMore": true,
  "nextOffset": 2,
  "posts": [
    {
      "slug": "how-we-chose-hosting",
      "author": "Anton Kravtsov",
      "publishedAt": "2026-08-10T00:00:00.000Z",
      "title": { "en": "How we chose hosting and got it wrong twice" },
      "image": { "en": "https://cdn.example.com/storage/your-site/blog/hosting-cover.jpg" }
    },
    {
      "slug": "noise-cancelling-explained",
      "author": null,
      "publishedAt": "2026-07-28T00:00:00.000Z",
      "title": { "en": "Noise cancelling, explained without the physics lecture" },
      "image": { "en": "https://cdn.example.com/storage/your-site/blog/anc-cover.jpg" }
    }
  ]
}

Поля відповіді

totalnumber
Скільки дописів видно просто зараз — чернетки й заплановані дописи не враховуються. Саме те для «29 статей» під заголовком.
limitnumber
Розмір вікна, який використано насправді, — вже з урахуванням значень за замовчуванням і меж.
offsetnumber
Скільки дописів пропущено. З page/perPage обчислюється за вас.
pagenumber | null
Номер поточної сторінки, починаючи з 1. null, коли offset не кратний limit (скажімо, limit=10&offset=5) — чесного номера сторінки тут просто немає.
pagesnumber
Загальна кількість сторінок: ceil(total / limit). 0 для порожнього блогу.
hasMoreboolean
Чи є дописи після цього вікна. Ваша кнопка «Показати ще» живе рівно доти, доки тут true.
nextOffsetnumber | null
offset для наступного вікна або null, коли ви дійшли до кінця. Передавайте його просто в наступний запит.
postsPost[]
Дописи цього вікна в порядку, заданому в CRM. Текстів тут немає — для них запитуйте окремий допис.
posts →
slugstring
Адреса допису, наприклад how-we-chose-hosting. Використовуйте її у своїх URL і для GET /v1/blog/:slug.
authorstring | null
Ім’я автора, як його ввели в CRM, або null, якщо допис ніхто не підписав.
publishedAtstring (ISO 8601) | null
Дата публікації або null, якщо її немає.
title{ [lang]: string }
Заголовок допису за мовами. Порожні переклади пропускаються, тож якоїсь мови може просто не бути — див. функцію із запасною мовою нижче.
image{ [lang]: string }
URL обкладинки за мовами. Якщо обкладинка одна, той самий URL повторюється для кожної запитаної мови, тож image[lang] можна читати завжди.

Пагінація: нескінченна прокрутка чи нумеровані сторінки

API розмовляє обома діалектами пагінації, тож перекладати один на інший у голові не доведеться. Беріть той, що пасує до вашого дизайну.

Нескінченна прокрутка і «Показати ще»: limit + offset

Запитайте перше вікно, покажіть його, а коли відвідувач прокрутить донизу (або натисне кнопку), запитайте наступне, починаючи з nextOffset. Коли nextOffset дорівнює null — готово.

# перше вікно
curl "https://back.sitecog.com/content/v1/blog?lang=en&limit=12" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"

# наступне вікно: offset = nextOffset з попередньої відповіді
curl "https://back.sitecog.com/content/v1/blog?lang=en&limit=12&offset=12" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"

Нумеровані сторінки: page + perPage

Класична пагінація «1 2 3 … 15». Надсилаєте номер сторінки, отримуєте pages, щоб намалювати посилання. Ось сторінка 3 по 10 дописів на сторінку, з 29:

curl "https://back.sitecog.com/content/v1/blog?lang=en&page=3&perPage=10" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"
200 OKjson
{
  "total": 29,
  "limit": 10,
  "offset": 20,
  "page": 3,
  "pages": 3,
  "hasMore": false,
  "nextOffset": null,
  "posts": [
    { "slug": "…", "author": "…", "publishedAt": "…", "title": { "en": "…" }, "image": { "en": "…" } }
  ]
}

Зверніть увагу: відповідь завжди описує вікно обома діалектами — page і pages для нумерованих посилань, limit, offset і nextOffset для прокрутки.

Коли стилі зустрічаються

Надіслали limit і perPage разом? Перемагає perPage. Надіслали offset і page? Перемагає page. Помилки не буде в жодному разі — але зробіть собі послугу й тримайтеся одного стилю в межах запиту.

Які дописи видно і в якому порядку

API показує рівно те, що має побачити відвідувач, і нічого з того, над чим редактор ще працює:

  • Лише опубліковані дописи. Чернетки ніколи не залишають CRM, хоч які параметри ви надішлете.
  • Заплановані дописи чекають своєї черги. Якщо в CRM увімкнено планування, допис із майбутньою датою публікації лишається прихованим до цього моменту. Через кешування він може з’явитися на кілька хвилин пізніше (приблизно до п’яти) — плануйте запуски з урахуванням цього.
  • Порядок задається в CRM. Вирішують налаштування блогу: ручний порядок (редактори перетягують дописи), за датою публікації чи за датою створення, за зростанням або спаданням. API повертає дописи саме в такому порядку; параметра сортування немає, тож головний тут — редактор.

Отримати один допис

GET/v1/blog/:slug

https://back.sitecog.com/content/v1/blog/:slug

Повний допис за його slug: усе, що є в списку, плюс body — сама стаття в HTML.

Параметри

slugpathstringобовʼязково
Slug допису зі списку, наприклад how-we-chose-hosting. Малі латинські літери, цифри й дефіси, до 120 символів. Будь-що інше дає 404 post_not_found.
langquerystringнеобовʼязковоЗа замовчуванням: усі активні мови
Які переклади включити в title, image та body. Правила ті самі, що й скрізь, — див. Мови.
x-crm-keyheaderstringобовʼязково
Ключ вашого сайту або ?key=, якщо заголовки недоступні.

Приклад запиту

curl "https://back.sitecog.com/content/v1/blog/how-we-chose-hosting?lang=en,de" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"

Приклад відповіді

200 OKjson
{
  "post": {
    "slug": "how-we-chose-hosting",
    "author": "Anton Kravtsov",
    "publishedAt": "2026-08-10T00:00:00.000Z",
    "title": {
      "en": "How we chose hosting and got it wrong twice",
      "de": "Wie wir Hosting gewählt haben"
    },
    "image": {
      "en": "https://cdn.example.com/storage/your-site/blog/hosting-cover.jpg",
      "de": "https://cdn.example.com/storage/your-site/blog/hosting-cover.jpg"
    },
    "body": {
      "en": "<p>Attempt one was the cheapest server we could find.</p><h2>What went wrong</h2><p>Everything, on a Friday night.</p>"
    }
  }
}

Бачите, що немає body.de? Німецький текст ще не написали, а порожні переклади в мапах блогу пропускаються, а не повертаються як "". А от єдина обкладинка повторюється для обох мов.

Поля відповіді

postobject
Сам допис, загорнутий в один ключ.
post →
slugstring
Адреса допису — та, яку ви запитали.
authorstring | null
Ім’я автора з CRM або null.
publishedAtstring (ISO 8601) | null
Дата публікації або null. Для людей її відформатує new Date(post.publishedAt).toLocaleDateString(lang).
title{ [lang]: string }
Заголовок за мовами. Порожні переклади пропускаються.
image{ [lang]: string }
URL обкладинки за мовами; єдина обкладинка повторюється для кожної запитаної мови.
body{ [lang]: string }
Стаття як HTML-рядок, за мовами. Порожні переклади пропускаються. Як її вивести — одразу нижче.

Безпечне виведення HTML-тексту

body — готовий HTML: заголовки, абзаци, списки, посилання, зображення. Щоб вставити його на сторінку, знадобиться перемикач вашого фреймворку «так, мені справді потрібен сирий HTML» — dangerouslySetInnerHTML у React, v-html у Vue.

Запасної мови на сервері немає: якщо перекладу бракує, ключа просто немає. Це закриває крихітна функція — запитана мова, потім ваша основна, потім будь-яка непорожня:

const t = (map, lang, fallback = 'en') =>
  map?.[lang] || map?.[fallback] || Object.values(map ?? {})[0] || '';

export function Post({ post, lang }) {
  return (
    <article>
      <h1>{t(post.title, lang)}</h1>
      {/* body очищає Diil під час збереження допису */}
      <div className="prose" dangerouslySetInnerHTML={{ __html: t(post.body, lang) }} />
    </article>
  );
}

Блог на Next.js: головна блогу і сторінка допису

Повноцінний блог на Next.js з headless CMS у трьох файлах: невеликий шар даних, головна з нумерованими сторінками і сторінка допису, заздалегідь відрендерена для кожного slug через generateStaticParams. Ключ лишається на сервері.

// lib/blog.ts — усе про блог в одному місці
const API = 'https://back.sitecog.com/content/v1';
const LANG = 'en';
const headers = { 'x-crm-key': process.env.CRM_KEY! };

export type LangMap = Record<string, string>;
export type Post = {
  slug: string;
  author: string | null;
  publishedAt: string | null;
  title: LangMap;
  image: LangMap;
  body?: LangMap;
};

export const t = (map: LangMap | undefined, lang = LANG, fallback = 'en') =>
  map?.[lang] || map?.[fallback] || Object.values(map ?? {})[0] || '';

export async function getPosts(page = 1, perPage = 12) {
  const res = await fetch(`${API}/blog?lang=${LANG}&page=${page}&perPage=${perPage}`, {
    headers,
    next: { revalidate: 60 },
  });
  if (!res.ok) throw new Error(`Content API: ${res.status}`);
  return (await res.json()) as { total: number; page: number | null; pages: number; posts: Post[] };
}

export async function getPost(slug: string) {
  const res = await fetch(`${API}/blog/${slug}?lang=${LANG}`, {
    headers,
    next: { revalidate: 60 },
  });
  if (res.status === 404) return null;
  if (!res.ok) throw new Error(`Content API: ${res.status}`);
  return ((await res.json()) as { post: Post }).post;
}

// limit не більше 50, тож обходимо весь блог через nextOffset
export async function getAllSlugs() {
  const slugs: string[] = [];
  let offset: number | null = 0;
  while (offset !== null) {
    const res = await fetch(`${API}/blog?lang=${LANG}&limit=50&offset=${offset}`, { headers });
    if (!res.ok) throw new Error(`Content API: ${res.status}`);
    const data: { posts: Post[]; nextOffset: number | null } = await res.json();
    slugs.push(...data.posts.map((post) => post.slug));
    offset = data.nextOffset;
  }
  return slugs;
}

Помилки

СтатусТілоЩо сталося
400{"message":"invalid_lang", …}Код у lang не схожий на код мови.
400{"message":"unknown_lang", …}Мова з lang не активна на сайті. У тілі є список доступних.
400{"message":"too_many_langs","max":50}Понад 50 кодів у lang. Вражає, але ні.
401{"message":"invalid_key"}Ключа немає, він некоректний або відкликаний.
404{"message":"post_not_found"}Немає видимого допису з таким slug — одруківка, slug із забороненими символами або допис, що є чернеткою чи ще не опублікований.
405{"message":"method_not_allowed"}Будь-що, крім GET чи HEAD. API лише для читання.
429{"message":"rate_limit_exceeded"}Забагато запитів за цю хвилину. Див. Ліміти запитів.

Некоректних значень пагінації в цьому списку немає навмисно: замість помилки вони відкочуються до значень за замовчуванням. Усе інше — на сторінці Помилки.

Поради з передової