Справочник
Уебкуки
Когато редактор публикува, скрие или изтрие запис, Citron изпраща кратко подписано съобщение до сайта ви, за да обнови той само страниците, които са се променили.
Настройване на уебкука#
- Добавете адресаВ конзолата отворете Настройки → Разработчици → Уебкуки и добавете адреса си, например
https://example.com/api/citron-webhook. Той трябва да е публиченhttp(s)адрес. - Изберете събития и колекцииИзберете някои от
entry.published,entry.unpublishedиentry.deleted. Оставете списъка с колекции празен, за да получавате за всички колекции. - Запазете тайния ключКопирайте генерирания таен ключ (
whsec_…) в средата на сайта си, напримерCITRON_WEBHOOK_SECRET. С него се подписва всяка доставка.
Събития#
| Събитие | Кога се изпраща |
|---|---|
entry.published | Запис е публикуван, включително при насрочено публикуване и при нова версия на публикуван запис. |
entry.unpublished | Публикуван запис е свален от сайта. |
entry.deleted | Публикуван запис е изтрит. Изтриването на запис, който никога не е публикуван, не изпраща нищо, защото никоя страница не го е показвала. |
Това са единствените три събития, които уебкуката получава. Новите видове събития ще се включват поотделно за всяка уебкука, така че съществуващ адрес никога не получава съобщение, което не е поискал.
Заявката#
Всяка доставка е POST с JSON тяло и три заглавки: content-type: application/json, x-citron-event (името на събитието) и x-citron-signature. Citron чака отговора ви до 10 секунди и не чете тялото му.
POST /api/citron-webhook HTTP/1.1
content-type: application/json
x-citron-event: entry.published
x-citron-signature: sha256=a8db6ae35854b938a4493cfb12b30f5db059390af0a89e9057a10f19ed14fe99
{"event":"entry.published","organizationId":"cmf2x0r1k0000s6014h3j5k7l","collection":"products","entry":{"id":"cmf3k9x2p0004s601q8w7e2rt","slug":"bio-6"},"at":"2026-09-21T09:30:00.000Z"}{
"event": "entry.published",
"organizationId": "cmf2x0r1k0000s6014h3j5k7l",
"collection": "products",
"entry": { "id": "cmf3k9x2p0004s601q8w7e2rt", "slug": "bio-6" },
"at": "2026-09-21T09:30:00.000Z"
}| Поле | Значение |
|---|---|
event | Името на събитието, същото като в x-citron-event. |
organizationId | Идентификаторът на работното ви пространство. |
collection | Slug на колекцията, както в /v1/content/{collection}. |
entry | { id, slug } на записа. slug може да е null. |
at | Кога е станала промяната (ISO 8601). |
Ключовете и редът им са фиксирани: подписът се изчислява точно върху тези байтове, така че формата само ще се разширява, никога няма да се променя.
Проверка на подписа#
x-citron-signature е sha256=, последвано от HMAC-SHA256 на суровото тяло на заявката в шестнадесетичен вид, с тайния ключ на уебкуката. Изчислете го върху получените байтове, преди да разчетете JSON, и сравнявайте за постоянно време. Ако не съвпада, отговорете с 401.
Заглавката няма времеви печат. Полето at е покрито от подписа, така че можете да отхвърляте стари доставки по възраст; оставете щедър прозорец, защото доставките чакат на опашка, докато работният процес на Citron не е на линия.
import { constructWebhookEvent, WebhookVerificationError } from '@citron/sdk/webhooks';
// Any runtime with Request/Response: Node 18+, edge, Workers
export async function handleCitronWebhook(req: Request): Promise<Response> {
const raw = await req.text(); // the RAW body: re-serialised JSON has different bytes
try {
const event = await constructWebhookEvent(raw, req.headers.get('x-citron-signature'), process.env.CITRON_WEBHOOK_SECRET!, {
maxAgeSeconds: 24 * 3600, // optional; the signed "at" makes old deliveries refusable
});
console.log(event.event, event.collection, event.entry.slug);
return new Response(null, { status: 204 });
} catch (err) {
if (err instanceof WebhookVerificationError) return new Response(null, { status: 401 });
throw err;
}
}import { createHmac, timingSafeEqual } from 'node:crypto';
/** Without the SDK: HMAC-SHA256 of the raw body bytes, hex, prefixed with "sha256=". */
export function isValidCitronSignature(rawBody: Buffer, header: string | undefined, secret: string): boolean {
const expected = Buffer.from('sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'));
const given = Buffer.from(header ?? '');
return given.length === expected.length && timingSafeEqual(given, expected);
}За да тествате обработчика си, подпишете тяло сами: signWebhookBody(body, secret) от @citron/sdk/webhooks връща стойността на заглавката. Примерът по-горе е подписан с тайния ключ whsec_3Jx9mQ2vR7tLp8Kd4Nw6Yb1Hs5Fc0Zeq.
Обновяване на Next.js сайт#
SDK съдържа готов route handler за App Router. Слагайте тагове на заявките си с citronTags(collection, slug) и при промяна на запис обработчикът обновява точно тези тагове. Страниците остават статични, докато редактор не публикува.
// app/api/citron-webhook/route.ts
import { revalidateTag } from 'next/cache';
import { citronTags, createRevalidateHandler } from '@citron/sdk/next';
// 503 without a secret, 401 for a bad signature, 500 if onEvent throws (the API retries)
export const POST = createRevalidateHandler({
secret: process.env.CITRON_WEBHOOK_SECRET,
onEvent: (e) => citronTags(e.collection, e.entry.slug).forEach((tag) => revalidateTag(tag)),
});import { citronTags } from '@citron/sdk/next';
// Tag the fetch so the webhook above can revalidate exactly this page:
// citronTags('products', 'bio-6') → ['citron:products', 'citron:products:bio-6']
const product = await citron.collection('products').find('bio-6', {
fetchOptions: { next: { revalidate: 3600, tags: citronTags('products', 'bio-6') } },
});
console.log(product?.slug);Повторни опити#
Всеки отговор 2xx се смята за доставка. При изтекло време, мрежова грешка или друг статус се правят още два опита: след 10 секунди и след още една минута. След третия опит доставката се изоставя. Адрес, който сочи към частна мрежа, се отказва и не се опитва отново.
- Отговаряйте бързо и вършете бавната работа след това; обработчик, който отнема повече от 10 секунди, получава повторен опит, дори да е приключил.
- Направете обработчика идемпотентен. Двойното обновяване на таг е безвредно.
- Върнете
500, когато вашата работа се провали и искате нов опит; обработчикът от SDK прави това, когатоonEventхвърли грешка.
Справочникът описва само публичното API и следва договора му за съвместимост: публичните отговори само получават нови полета. Описанието на тези адреси във формат OpenAPI е на /developers/openapi.json.