Nhà phát triểnAdapters
db (site có database)
Site runtime (Next SSR / Laravel / Astro SSR) — nhận bài, upsert vào DB, live ngay.
Dùng khi: site có runtime + database (Next.js SSR, Laravel, Astro SSR, Node/Nest, Rails…). Case đơn giản nhất: publish xong live ngay, không cần rebuild.
Endpoint nhận bài → upsert vào DB của bạn → render động. Trả 200 { status: "live" }.
Chọn nền DB (ít bẫy nhất trước — tránh cold-pause): Cloudflare D1 (nếu ở CF Pages, không pause) › Neon / Turso (scale-to-zero, resume nhanh) › Supabase (⚠️ free pause sau 7 ngày → BẮT BUỘC kèm keep-alive cron ngoài). Nếu bạn "chỉ muốn có bài, khỏi backend" → dùng citely-api.
Handshake (GET)
{
"citely": "receiver",
"protocol": 1,
"profile": "runtime-db",
"storageAdapter": "db",
"accepts": ["html"],
"locales": ["vi", "en"],
"async": false
}accepts: ["html"] — site động thường lưu & render thẳng html. async: false → POST trả
200 { status: "live" }.
Nhận bài (POST) — Next.js App Router
// app/api/citely/ingest/route.ts
export async function GET() {
return Response.json({
citely: 'receiver', protocol: 1, profile: 'runtime-db',
storageAdapter: 'db', accepts: ['html'], locales: ['vi', 'en'], async: false,
});
}
export async function POST(req: Request) {
const raw = await req.text(); // RAW body trước khi parse
const h = Object.fromEntries(req.headers) as Record<string, string>;
if (!verify(process.env.CITELY_WEBHOOK_SECRET!, raw, h)) {
return Response.json({ error: 'CPP_SIG_INVALID' }, { status: 401 });
}
const a = JSON.parse(raw);
if (a.version !== 1) {
return Response.json({ error: 'CPP_VERSION_UNSUPPORTED' }, { status: 400 });
}
if (a.test === true) {
return Response.json({ ok: true });
}
try {
const post = await db.posts.upsert({
where: a.externalId ? { id: a.externalId } : { slug: a.slug },
create: {
title: a.title, slug: a.slug, body: a.html, excerpt: a.excerpt,
jsonLd: a.jsonLd, cover: a.cover, locale: a.locale,
translationKey: a.translationKey, tags: a.tags,
author: a.author?.name, publishedAt: a.publishedAt, noindex: a.noindex,
},
update: {
title: a.title, body: a.html, excerpt: a.excerpt,
jsonLd: a.jsonLd, cover: a.cover, tags: a.tags, modifiedAt: a.modifiedAt,
},
});
return Response.json({ id: String(post.id), url: `https://site.com/blog/${a.slug}`, status: 'live' });
} catch (e) {
return Response.json({ error: 'CPP_STORAGE_FAILED', message: String(e) }, { status: 500 });
}
}verify = hàm Standard Webhooks.
Lưu ý
- JSON-LD: lưu
a.jsonLd(mảng), emit mỗi phần tử thành<script type="application/ld+json">trong<head>. Đừng nhét vàobodyhtml. - ISR/cache (Next): nếu trang blog cache tĩnh, gọi
revalidatePath('/blog/' + slug)sau upsert. - Idempotency: dedupe theo header
webhook-idnếu muốn tuyệt đối an toàn khi Citely retry.