Diil Docs
  1. Документація
  2. Посібники

Кеш, ETag і 304: швидко й завжди свіже

Оновлено:

Content API зазвичай змушують обирати: швидко або свіжо. Ми б не хотіли. Відповіді кешуються на нашому боці, але там ніколи не застарівають, кожна відповідь має ETag, тож незмінений контент майже нічого вам не коштує, а правка в CRM потрапляє в API тієї ж миті, коли редактор натискає «Зберегти». Ця сторінка пояснює, як це працює і як вичавити з цього максимум на вашому боці.

Від правки в CRM до вашої сторінки

Ось увесь шлях зміни — від клавіатури редактора до екрана вашого відвідувача:

  1. Редактор зберігає

    Хтось виправляє одруківку в CRM або просто на живому сайті. Рахується будь-яка правка — текст, зображення, налаштування сторінки, допис у блозі.
  2. Версія контенту сайту зростає

    Кожна правка підвищує версію контенту сайту. Наш серверний кеш (Redis) прив’язаний до цієї версії, тож усі старі закешовані відповіді одразу стають неактуальними. Нікому не треба «чистити кеш».
  3. Наступний запит до API отримує свіжі дані

    Найближчий запит до API збирає відповідь уже з нового контенту. На нашому боці жодної затримки: ні TTL, який треба перечекати, ні черги на очищення.
  4. Кеші між нами й відвідувачем наздоганяють

    Те, що відвідувач бачить насправді, може трохи відставати: браузер чи CDN може тримати попередню відповідь до 60 секунд і ще раз показати її, поки тихенько завантажує нову у фоні (це і є stale-while-revalidate). Ваш власний серверний кеш, якщо він є, додає зверху свій час життя.

Заголовки кешування по черзі

Типова успішна відповідь приходить із такими заголовками:

200 OK — заголовки відповідіhttp
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 з порожнім тілом. Ваш код і далі користується копією, яка в нього вже є.

Перший запит: повна відповідьhttp
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": { … } }
Наступний запит: нічого не змінилосяhttp
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.

Браузер — нічого налаштовувати не требаjs
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 і завантажуйте тіло, лише якщо воно змінилося.

lib/content.ts — Express, Fastify, Nuxt, Remix, будь-що на Nodets
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 тримає відповідь хвилину, потім оновлює її у фоні.

app/page.tsxtsx
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 — але стежте за лімітами. Хочете кнопку «опублікувати зараз» для великого запуску? Позначте свої запити тегом і зробіть невеликий маршрут, що скидає цей тег:

app/api/revalidate/route.tsts
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',
});

Більше про режим Live і розмітку за ним — на сторінці Віджет і розмітка.

«Чому я не бачу свою правку?»

Найпопулярніше питання про будь-який кеш, відколи існують кеші. Пройдіться списком згори донизу — він іде від «десять секунд на перевірку» до «зробіть собі чаю».

  1. Спитайте API напряму. Звичайний curl не має кешу, а наш бік завжди свіжий. Якщо curl показує новий текст, з API все гаразд, а стара копія живе в якомусь кеші дорогою. Якщо curl показує старий текст, ідіть далі списком.
  2. А правку справді збережено? Перевірте в CRM. Для дописів у блозі: чернетки ніколи не повертаються, а допис, запланований на майбутнє, лишається прихованим до своєї дати (і може з’явитися на кілька хвилин пізніше).
  3. Той сайт? Ключ належить рівно одному сайту. Staging і production з різними ключами читають різний контент.
  4. Та мова? Запасної мови на сервері немає. Якщо редактор змінив німецький текст, а ви рендерите англійський, видимо нічого не станеться. Порожній переклад повертається як "", і ваш код із запасною мовою може тихенько показати замість нього іншу мову. Див. Мови та запасні варіанти.
  5. Той блок? Маркери чутливі до регістру, а маркери блоків унікальні лише в межах секції — /v1/blocks/title без ?section= повертає найстаріший блок із цим маркером, і це може бути зовсім не той, який редагували.
  6. Прихована секція? Секції з show: false теж повертаються. Якщо редактор сховав секцію, а вона досі на сайті, ваш шаблон не перевіряє show.
  7. Ваш власний кеш. revalidate, ISR, мапа в пам’яті, CDN перед вашим сайтом, сторінка, зібрана один раз під час збірки. Це головний підозрюваний.
  8. Браузер. До 60 секунд плюс одне фонове оновлення. Жорстке перезавантаження (Ctrl+Shift+R або Cmd+Shift+R) усе вирішує.
  9. Редагуєте в режимі Live? Переконайтеся, що всередині фрейму CRM ви запитуєте з cache: 'no-store' (див. вище).
Що API повертає просто заразbash
curl -i "https://back.sitecog.com/content/v1/pages/home?lang=en" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"