Citely Docs
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ào body html.
  • ISR/cache (Next): nếu trang blog cache tĩnh, gọi revalidatePath('/blog/' + slug) sau upsert.
  • Idempotency: dedupe theo header webhook-id nếu muốn tuyệt đối an toàn khi Citely retry.

On this page