Nhà phát triển
Envelope (CPP v1)
Cấu trúc JSON canonical của một bài viết mà Citely gửi ra khi publish.
Mỗi lần publish, Citely gửi một JSON body mô tả trọn vẹn một bài viết. Envelope là
superset & self-sufficient: mọi thứ để render một bài đều nằm trong một POST —
website không bao giờ phải gọi ngược Citely để lấy thêm.
Quy tắc bất biến
- Forward-compatible. Citely có thể thêm field mới bất cứ lúc nào trong cùng
version. Receiver PHẢI bỏ qua field lạ, không được lỗi vì gặp field chưa biết. - Optional trừ khi ghi "bắt buộc". Field vắng mặt = không có dữ liệu đó; đừng suy diễn.
Chỉ
version,event,title,slug,statuslà luôn có. version= phiên bản envelope. CPP v1 dùngversion: 1. Chỉ bump khi có breaking change (đổi nghĩa/xoá field) — thêm field KHÔNG bump.- HTML sạch, JSON-LD tách riêng.
htmllà thân bài ngữ nghĩa, không chứa<script type="application/ld+json">. Schema.org nằm ở mảngjsonLd[]để receiver tự đặt vào<head>.
Body (JSON)
{
"version": 1,
"event": "article.publish",
// ── Định danh & upsert ──────────────────────────────────────────────
"externalId": "string | null", // id RECEIVER đã trả lần trước → UPDATE bản đó. null → CREATE mới.
"title": "string", // bắt buộc
"slug": "string", // bắt buộc, đã kebab-case/ASCII-hoá
// ── Nội dung (receiver chọn định dạng nó "accepts") ─────────────────
"html": "string", // HTML ngữ nghĩa, render as-is (đã sạch Gutenberg)
"markdown": "string", // cùng bài, dạng Markdown (tiện cho SSG)
// ── Meta SEO ────────────────────────────────────────────────────────
"excerpt": "string", // meta description (~150–160 ký tự)
"focusKeyword": "string",
"tags": ["string"], // topic cluster / chủ đề
"author": { "name": "string", "url": "string | null", "avatar": "string(url) | null" },
"canonicalUrl": "string | null",
"noindex": false, // true → receiver set noindex + KHÔNG lên sitemap
// ── Đa ngôn ngữ ─────────────────────────────────────────────────────
"locale": "vi", // BCP-47 gọn: "vi" | "en" | "vi-VN"
"translationKey": "string|null", // nối hreflang giữa các bản dịch cùng 1 bài gốc
// ── Media (URL tuyệt đối trên CDN Citely) ───────────────────────────
"cover": "string(url) | null", // ảnh đại diện / OG
"images": [{ "url": "string(url)", "alt": "string", "caption": "string" }],
// ── JSON-LD (Schema.org, đã tách khỏi html) ─────────────────────────
"jsonLd": [{ "@context": "https://schema.org", "@type": "Article" }],
// ── Trạng thái & thời gian ──────────────────────────────────────────
"status": "publish", // "publish" | "draft" | "future"
"publishedAt": "ISO-8601 | undefined",
"modifiedAt": "ISO-8601 | undefined",
"test": false // true = ping "Test Connection". Verify rồi trả 200, KHÔNG lưu.
}Ghi chú field
| Field | Ghi chú cho receiver |
|---|---|
externalId | Khoá upsert số 1. Khác null → cập nhật đúng bản ghi đó. null → tạo mới. Không lưu id thì upsert theo slug. |
html vs markdown | Chọn theo accepts bạn khai ở handshake. Site tĩnh thường lấy markdown; site động thường lưu html. |
tags | Chuỗi tự do (không phải id). Map vào taxonomy của bạn; bỏ qua nếu schema không có. |
author | Render byline + Person trong JSON-LD. jsonLd[] đã chứa Article/Person, nên author chỉ là tiện lợi. |
locale / translationKey | Site song ngữ: locale → trường lang; các bài cùng translationKey là bản dịch của nhau → nối <link rel="alternate" hreflang>. |
cover / images[].url | URL tuyệt đối trên CDN Citely. Mặc định hot-link (0 việc). Muốn bền thì tải về R2/repo của bạn. |
jsonLd[] | Emit mỗi phần tử thành một <script type="application/ld+json"> trong <head>. Đừng nhét lại vào html. |
status: "future" | Bài hẹn giờ. publishedAt là thời điểm tương lai → giữ nháp tới giờ đó. |
Field-mapping (bước AI của website làm)
AI của repo phải đọc schema thật (vd src/content.config.ts Zod của Astro, hay bảng DB)
rồi map field CPP → field local. Ví dụ:
| CPP field | Frontmatter Astro (Zod) |
|---|---|
title | title |
excerpt | description |
publishedAt | pubDate |
modifiedAt | updatedDate |
cover | heroImage |
locale | lang |
tags | tags |
author.name | author |
Field CPP không có trường tương ứng → bỏ qua. Trường Zod bắt buộc mà CPP không cấp → điền
default hợp lý (vd pubDate = publishedAt ?? now).
Bước tiếp theo: Receiver endpoint — endpoint website phải dựng để nhận envelope này.