Content API зазвичай змушують обирати: швидко або свіжо. Ми б не хотіли. Відповіді кешуються на нашому боці, але там ніколи не застарівають, кожна відповідь має ETag, тож незмінений контент майже нічого вам не коштує, а правка в CRM потрапляє в API тієї ж миті, коли редактор натискає «Зберегти». Ця сторінка пояснює, як це працює і як вичавити з цього максимум на вашому боці.
Від правки в CRM до вашої сторінки
Ось увесь шлях зміни — від клавіатури редактора до екрана вашого відвідувача:
Редактор зберігає
Хтось виправляє одруківку в CRM або просто на живому сайті. Рахується будь-яка правка — текст, зображення, налаштування сторінки, допис у блозі.Версія контенту сайту зростає
Кожна правка підвищує версію контенту сайту. Наш серверний кеш (Redis) прив’язаний до цієї версії, тож усі старі закешовані відповіді одразу стають неактуальними. Нікому не треба «чистити кеш».Наступний запит до API отримує свіжі дані
Найближчий запит до API збирає відповідь уже з нового контенту. На нашому боці жодної затримки: ні TTL, який треба перечекати, ні черги на очищення.Кеші між нами й відвідувачем наздоганяють
Те, що відвідувач бачить насправді, може трохи відставати: браузер чи CDN може тримати попередню відповідь до 60 секунд і ще раз показати її, поки тихенько завантажує нову у фоні (це і єstale-while-revalidate). Ваш власний серверний кеш, якщо він є, додає зверху свій час життя.
Заголовки кешування по черзі
Типова успішна відповідь приходить із такими заголовками:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60, stale-while-revalidate=600
ETag: W/"a41f9c0e7b2d58f3"
Vary: x-crm-key, Accept-Encoding
Access-Control-Allow-Origin: *| Заголовок | Що він каже кешам |
|---|---|
Cache-Control: public | Зберігати відповідь може будь-хто: браузер, CDN, проксі. Контент і так публічний — це те, що ви показуєте на сайті. |
max-age=60 | Протягом 60 секунд відповідь вважається свіжою, і її можна використовувати повторно, взагалі нас не питаючи. |
stale-while-revalidate=600 | Наступні 10 хвилин кеш може миттєво віддавати стару копію, завантажуючи нову у фоні. Швидко для цього відвідувача, свіжо для наступного. |
ETag: W/"…" | Слабкий ETag — хеш тіла відповіді. Те саме тіло — той самий ETag. Надішліть його назад у If-None-Match, і якщо нічого не змінилося, отримаєте 304. |
Vary: x-crm-key, Accept-Encoding | Кеші мусять тримати окремі копії для кожного ключа сайту й кожного стиснення. Два сайти ніколи не ділять закешовану відповідь, навіть для однакового URL. |
Cache-Control: no-store | Надсилається з кожною помилкою. 404 для сторінки, яку редактор створює просто зараз, не повинен застрягти в чиємусь кеші. |
CORS відкритий для будь-якого джерела, API явно дозволяє заголовок запиту If-None-Match і відкриває ETag для JavaScript — тож усе на цій сторінці працює і з браузера.
ETag і 304 Not Modified
ETag — найдешевший спосіб спитати «щось змінилося?». Запам’ятайте ETag з останньої відповіді, наступного разу надішліть його в If-None-Match, і якщо контент той самий, ви отримаєте 304 Not Modified з порожнім тілом. Ваш код і далі користується копією, яка в нього вже є.
GET /content/v1/pages/home?lang=en HTTP/1.1
Host: back.sitecog.com
x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
HTTP/1.1 200 OK
Cache-Control: public, max-age=60, stale-while-revalidate=600
ETag: W/"a41f9c0e7b2d58f3"
Vary: x-crm-key, Accept-Encoding
{ "id": 26, "marker": "home", "name": "Home", "content": { … } }GET /content/v1/pages/home?lang=en HTTP/1.1
Host: back.sitecog.com
x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
If-None-Match: W/"a41f9c0e7b2d58f3"
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60, stale-while-revalidate=600
ETag: W/"a41f9c0e7b2d58f3"Щойно редактор щось змінить на цій сторінці, зміниться тіло, зміниться хеш, і той самий запит поверне свіжий 200 з новим ETag.
Рецепти кешування
У браузері: усе вже зроблено
Якщо ви запитуєте контент просто в браузері, не потрібно жодного рядка коду для кешування. Звичайний fetch використовує HTTP-кеш браузера: 60 секунд він повторно використовує відповідь, а потім сам перевіряє актуальність через If-None-Match.
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
// cache: 'default' — і так значення за замовчуванням, max-age та ETag браузер бере на себе
});
const page = await res.json();Не дивуйтеся, що ваш код ніколи не бачить 304: браузер підміняє його закешованим 200 ще до того, як він до вас дійде. Відкрийте вкладку Network у DevTools, щоб побачити, що сталося насправді, — саме там ви знайдете 304 або «disk cache».
На сервері Node: крихітний кеш з ETag
Серверний fetch у Node не має власного HTTP-кешу, тож кожен виклик іде аж до API. Невеликий Map це виправляє: використовуйте копію 60 секунд (точно як max-age), потім питайте з If-None-Match і завантажуйте тіло, лише якщо воно змінилося.
const API = 'https://back.sitecog.com/content';
const TTL = 60_000; // як max-age=60
type Entry = { etag: string | null; data: unknown; at: number };
// Один процес, один ключ сайту. Використовуєте кілька ключів? Додайте ключ і в ключ кешу.
const cache = new Map<string, Entry>();
export async function getContent<T>(path: string): Promise<T> {
const url = API + path;
const cached = cache.get(url);
// 1. Досить свіже — навіть не звертаємося до API
if (cached && Date.now() - cached.at < TTL) return cached.data as T;
// 2. Питаємо «змінилося?» з ETag, який у нас уже є
const headers: Record<string, string> = { 'x-crm-key': process.env.CRM_KEY! };
if (cached?.etag) headers['if-none-match'] = cached.etag;
const res = await fetch(url, { headers });
if (res.status === 304 && cached) {
cached.at = Date.now(); // той самий контент, ще 60 секунд спокою
return cached.data as T;
}
if (!res.ok) throw new Error(`Content API ${res.status} for ${path}`);
const data = (await res.json()) as T;
cache.set(url, { etag: res.headers.get('etag'), data, at: Date.now() });
return data;
}
// використання
const home = await getContent('/v1/pages/home?lang=en');Next.js: revalidate та оновлення на вимогу
В App Router усю роботу робить кеш fetch. revalidate: 60 збігається з нашим max-age: Next.js тримає відповідь хвилину, потім оновлює її у фоні.
export default async function Home() {
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
headers: { 'x-crm-key': process.env.CRM_KEY! },
next: { revalidate: 60, tags: ['crm-content'] },
});
const page = await res.json();
return <h1>{page.content.hero.content.hero_title.content.en}</h1>;
}Хочете меншу затримку? Зменште revalidate — але стежте за лімітами. Хочете кнопку «опублікувати зараз» для великого запуску? Позначте свої запити тегом і зробіть невеликий маршрут, що скидає цей тег:
import { revalidateTag } from 'next/cache';
export async function POST(req: Request) {
if (req.headers.get('x-revalidate-secret') !== process.env.REVALIDATE_SECRET) {
return new Response('Nope', { status: 401 });
}
// Next.js 16 приймає другий аргумент; у Next.js 15 це просто revalidateTag('crm-content')
revalidateTag('crm-content', { expire: 0 });
return Response.json({ revalidated: true });
}Викликайте його звідки зручно вашій команді: зі скрипту деплою, букмарклета, чат-бота, кнопки у вашій власній адмінці. Секрет тільки ваш — до ключа сайту він не має жодного стосунку.
Режим Live: повз усі кеші
Коли редактор відкриває ваш сайт у режимі Live у CRM, він хоче бачити свою зміну одразу після збереження — а не за хвилину. Усередині фрейму CRM до URL додається ?crm_live=1. Хороша практика (і саме так робить наш еталонний клієнт): коли сторінка всередині iframe або має crm_live, запитуйте з cache: 'no-store'. Наш бік і так свіжий, тож більше нічого не потрібно.
const isLive =
window.self !== window.top ||
new URLSearchParams(location.search).has('crm_live');
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
cache: isLive ? 'no-store' : 'default',
});type Props = { searchParams: Promise<{ crm_live?: string }> };
export default async function Home({ searchParams }: Props) {
const live = (await searchParams).crm_live === '1';
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
headers: { 'x-crm-key': process.env.CRM_KEY! },
// редактори отримують свіжі дані на кожен запит, відвідувачі — закешовану копію
...(live ? { cache: 'no-store' as const } : { next: { revalidate: 60 } }),
});
const page = await res.json();
// …
}Більше про режим Live і розмітку за ним — на сторінці Віджет і розмітка.
«Чому я не бачу свою правку?»
Найпопулярніше питання про будь-який кеш, відколи існують кеші. Пройдіться списком згори донизу — він іде від «десять секунд на перевірку» до «зробіть собі чаю».
- Спитайте API напряму. Звичайний
curlне має кешу, а наш бік завжди свіжий. Якщо curl показує новий текст, з API все гаразд, а стара копія живе в якомусь кеші дорогою. Якщо curl показує старий текст, ідіть далі списком. - А правку справді збережено? Перевірте в CRM. Для дописів у блозі: чернетки ніколи не повертаються, а допис, запланований на майбутнє, лишається прихованим до своєї дати (і може з’явитися на кілька хвилин пізніше).
- Той сайт? Ключ належить рівно одному сайту. Staging і production з різними ключами читають різний контент.
- Та мова? Запасної мови на сервері немає. Якщо редактор змінив німецький текст, а ви рендерите англійський, видимо нічого не станеться. Порожній переклад повертається як
"", і ваш код із запасною мовою може тихенько показати замість нього іншу мову. Див. Мови та запасні варіанти. - Той блок? Маркери чутливі до регістру, а маркери блоків унікальні лише в межах секції —
/v1/blocks/titleбез?section=повертає найстаріший блок із цим маркером, і це може бути зовсім не той, який редагували. - Прихована секція? Секції з
show: falseтеж повертаються. Якщо редактор сховав секцію, а вона досі на сайті, ваш шаблон не перевіряєshow. - Ваш власний кеш.
revalidate, ISR, мапа в пам’яті, CDN перед вашим сайтом, сторінка, зібрана один раз під час збірки. Це головний підозрюваний. - Браузер. До 60 секунд плюс одне фонове оновлення. Жорстке перезавантаження (Ctrl+Shift+R або Cmd+Shift+R) усе вирішує.
- Редагуєте в режимі Live? Переконайтеся, що всередині фрейму CRM ви запитуєте з
cache: 'no-store'(див. вище).
curl -i "https://back.sitecog.com/content/v1/pages/home?lang=en" \
-H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"