Diil Docs
  1. Документация
  2. Справочник API

GET /v1/blog — статьи, страницы списка, одна статья

Обновлено:

Редакторы пишут статьи в CRM, ваш сайт их показывает. API блога отдаёт список по страницам — для ленты — и статью целиком по её адресу — для страницы статьи. Два запроса, и просьбы «опубликуй это к пятнице» больше не приходят вам в мессенджер.

Запроса два — по одному на каждый тип страницы, который вы будете собирать:

  • 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 }
Адрес обложки по языкам. Если обложка у статьи одна, её адрес повторяется для каждого запрошенного языка — image[lang] есть всегда.

Пагинация: бесконечная лента или страницы с номерами

API говорит на обоих диалектах пагинации, так что переводить один в другой в уме не придётся. Берите тот, что подходит вашему дизайну.

Бесконечная лента и «Показать ещё»: limit + offset

Запросите первое окно, покажите его, а когда посетитель долистает вниз (или нажмёт кнопку) — запросите следующее, начиная с 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, чтобы нарисовать ссылки. Вот третья страница по 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

Статья целиком по её адресу: всё то же, что в ленте, плюс body — сам текст в HTML.

Параметры

slugpathstringобязательно
Адрес статьи из ленты, например 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 }
Адрес обложки по языкам; единственная обложка повторяется для каждого запрошенного языка.
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 в трёх файлах: небольшой слой данных, лента со страницами и страница статьи, заранее собранная для каждого адреса через 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}В lang больше 50 кодов. Впечатляет, но нет.
401{"message":"invalid_key"}Ключа нет, он кривой или отозван.
404{"message":"post_not_found"}Видимой статьи с таким адресом нет: опечатка, запрещённые символы в адресе или статья ещё черновик либо не опубликована.
405{"message":"method_not_allowed"}Любой метод, кроме GET и HEAD. API только читает.
429{"message":"rate_limit_exceeded"}Слишком много запросов за эту минуту. Смотрите Лимиты.

Кривых параметров пагинации в списке нет намеренно: вместо ошибки они превращаются в значения по умолчанию. Всё остальное — на странице Ошибки.

Советы из окопов