# Citely Docs (/docs) Chào mừng đến với tài liệu **Citely** — nền tảng SaaS viết & xuất bản nội dung SEO/GEO. Tài liệu chia làm hai mảng: ## Bắt đầu nhanh [#bắt-đầu-nhanh] * **Bạn là người dùng Citely?** → [Hướng dẫn sử dụng](/docs/guides) * **Bạn là dev cần nhận bài từ Citely vào website?** → [Tổng quan Citely Connect](/docs/developer) > Tài liệu này là *docs-as-code* (mã nguồn mở, viết bằng MDX). Nội dung máy-đọc-được tại > [`/llms.txt`](/llms.txt) và [`/llms-full.txt`](/llms-full.txt) để trợ lý AI tra cứu nhanh. # Trạng thái bất đồng bộ (building → live) (/docs/developer/async-publish) Site động (`db`) publish xong là **live ngay**. Site tĩnh (`fs-build`) thì **bản chất bất đồng bộ**: nhận bài → ghi file → CI/build (30–90s) → mới hiển thị. CPP mô hình hoá điều này bằng **một field `status` + một flag `async`**, không phân mảnh. ## Ba trạng thái, một mô hình [#ba-trạng-thái-một-mô-hình] ```text db adapter POST → 200 { status: "live" } (xong, hiển thị ngay) fs-build POST → 202 { status: "building" } (đã nhận, đang build) │ └─ Citely POLL `url` (HEAD) tới khi 200 → tự chuyển "live". Site KHÔNG cần làm gì thêm. ``` ## Mặc định — Citely poll (site không làm gì) [#mặc-định--citely-poll-site-không-làm-gì] 1. Receiver ghi bài → trả `202 { id, url, status: "building" }`. 2. Citely lưu một *delivery record* (permalink + trạng thái `building`). 3. Citely định kỳ `GET url` (HEAD/GET). Khi trả `200` → chuyển record sang `live`. 4. UI Citely hiện "Đang build → Đã xuất bản". Đây là đường khuyến nghị: site tĩnh tối thiểu chỉ cần trả `202` rồi mặc kệ. Poll là cách DUY NHẤT đang khả dụng hôm nay — receiver không cần expose thêm endpoint nào. ## Vì sao không dùng trạng thái đồng bộ "giả" [#vì-sao-không-dùng-trạng-thái-đồng-bộ-giả] Có thể trả `200 {url}` ngay và coi như xong. Nhưng URL đó có thể `404` vài chục giây tới khi build xong → user bấm vào thấy lỗi, và Citely index sớm một URL chưa tồn tại. `202 building` * theo dõi là trung thực hơn: Citely chỉ báo "đã xuất bản" khi bài thật sự sống, và chỉ ping Google index khi `live`. Callback tức thì (site gọi Citely khi build xong) là **kế hoạch tương lai, chưa triển khai** — endpoint `POST /api/connect/live` chưa tồn tại. Hiện tại chỉ dùng đường poll ở trên. # Mã lỗi CPP_* (/docs/developer/errors) Receiver **không** trả lỗi tự do. Trả **mã chuẩn** để Citely map thẳng vào error-catalog của nó và hiện đúng cách xử lý cho user — thay vì một "500 Internal Error" mù mờ. Response lỗi: mã HTTP + JSON `{ "error": "", "message": "mô tả ngắn" }`. ## Mã lỗi receiver → Citely [#mã-lỗi-receiver--citely] | HTTP | `error` code | Khi nào | Citely xử lý / hiện cho user | | ---- | ------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------- | | 401 | `CPP_SIG_INVALID` | HMAC không khớp | "Secret lệch giữa Citely và site → xoay lại secret & cập nhật env." | | 401 | `CPP_REPLAY` | `abs(now - ts) > 300s` | "Đồng hồ server site lệch > 5 phút → chỉnh NTP." | | 400 | `CPP_VERSION_UNSUPPORTED` | Site chưa hỗ trợ `version`/`protocol` này | "Site cần nâng cấp receiver lên CPP v1." Citely chặn publish. | | 422 | `CPP_VALIDATION` | Payload thiếu/sai field bắt buộc (kèm `field`) | Hiện đúng field lỗi (vd `title` vượt giới hạn Zod của site). | | 409 | `CPP_CONFLICT` | Slug/externalId đụng bản ghi khác | "Trùng bài — kiểm tra externalId/slug." | | 429 | `CPP_RATE_LIMITED` | Site tự giới hạn tần suất | Citely backoff & retry. | | 500 | `CPP_STORAGE_FAILED` | Ghi thất bại (DB write / file) | "Lỗi phía site khi lưu bài." Citely retry. | | 502 | `CPP_UNREACHABLE` | Không kết nối được endpoint (timeout, DNS, WAF chặn) | "Không gọi được site — kiểm firewall/WAF/allowlist IP Citely." | Quy ước: mọi mã bắt đầu `CPP_`, SCREAMING\_SNAKE, **không đổi tên** khi đã phát hành (thêm mã mới nếu cần nghĩa mới). Với `CPP_VALIDATION`, kèm `"field"` để Citely chỉ đúng chỗ: ```json { "error": "CPP_VALIDATION", "message": "title too long (max 70)", "field": "title" } ``` ## Trạng thái publish (`status` trong echo) [#trạng-thái-publish-status-trong-echo] | `status` | Nghĩa | Adapter điển hình | | ---------- | -------------------------------------------- | ------------------ | | `live` | Bài đã hiển thị công khai tại `url` | `db` (render động) | | `building` | Đã nhận & lưu, đang chờ CI/build để hiển thị | `fs-build` | Chi tiết vòng đời `building → live`: [Trạng thái bất đồng bộ](/docs/developer/async-publish). ## Đối xứng hai chiều [#đối-xứng-hai-chiều] Citely dùng bộ mã này ở **cả hai chiều**: khi transport gặp lỗi lúc `POST` tới site, nó phân loại về đúng `CPP_*` và surface qua hệ lỗi của Citely. Cùng một ngôn ngữ lỗi từ Citely → site → về Citely, nên user luôn thấy thông điệp hành động được, không phải log thô. # Citely Connect — Tổng quan (/docs/developer) **Citely Connect** là cách Citely đẩy bài viết đã hoàn thiện ra **website của bạn**. **Citely Publish Protocol v1 (CPP v1)** là *hợp đồng kỹ thuật* mà website xây theo để nhận bài — Citely đặt chuẩn, website conform. Citely đóng vai CMS/bộ não nội dung. Nhiều landing page hiện đại (Astro, Next.js, Hugo, React thuần, Laravel…) **không có CMS quản trị blog**. Citely Connect lấp đúng chỗ đó: website chỉ cần dựng **một endpoint** theo CPP v1 là sẵn sàng nhận bài — không phải tự viết admin, editor, auth, storage. ## Kết nối bằng một dòng (copy cho AI) [#kết-nối-bằng-một-dòng-copy-cho-ai] Admin website **không cần tự code**. Copy đúng một dòng và dán cho AI của bạn (Claude Code / Cursor / Copilot): ```text Fetch https://citely-seo.com/connect/v1/agent.md — rồi nối repo này với Citely. ``` AI đó sẽ `Fetch` spec, tự nhận diện stack, chọn adapter, dựng endpoint, map field theo schema repo của bạn, và in ra hướng dẫn đặt `CITELY_WEBHOOK_SECRET`. ## Ba tầng của chuẩn [#ba-tầng-của-chuẩn] | Tầng | Ai định nghĩa | Nội dung | | ----------------- | ---------------- | ------------------------------------------------------------------------- | | 1. **Envelope** | Citely phát | 1 bài = 1 JSON canonical (html + markdown + jsonLd + meta + i18n + media) | | 2. **Receiver** | Website xây | 1 endpoint: `GET` handshake năng lực + `POST` nhận bài | | 3. **Vocabulary** | Cả hai nói chung | `status` + mã lỗi `CPP_*` | ## Chọn adapter theo "bạn muốn SỞ HỮU dữ liệu tới đâu" [#chọn-adapter-theo-bạn-muốn-sở-hữu-dữ-liệu-tới-đâu] | Adapter | Ai lưu bài | Website xây | Lên sóng | | --------------- | -------------------------- | --------------------------------------------------- | ----------- | | `citely-api` 🥇 | **Citely** (bạn 0 backend) | chỉ 1 *reader* (fetch + render), KHÔNG cần receiver | ngay | | `db` | DB của bạn | receiver upsert row → render động | ngay | | `fs-build` | đĩa/repo của bạn | receiver ghi file + trigger build | sau rebuild | * **`citely-api` = mặc định khuyến nghị:** bạn KHÔNG dựng endpoint — chỉ đăng ký site trong Citely, nhận read-key, rồi *đọc* read-API. Đây là adapter DUY NHẤT không cần receiver. * **`db`** cho bên muốn dữ liệu nằm trong DB của mình (Cloudflare D1 › Neon / Turso › Supabase). * **`fs-build`** cho site tĩnh self-host (ghi file + rebuild). Adapter `git` (commit mỗi bài) và `citely-git` (GitHub App) **đã bỏ/gác lại** — ngàn commit + rebuild mỗi lần publish không scale. ## Cây quyết định [#cây-quyết-định] ```text Bạn muốn SỞ HỮU dữ liệu tới đâu? ├─ "Chỉ cần có bài, khỏi backend" → citely-api (Citely host + read-API) ⭐ mặc định ├─ "Muốn bài trong DB của tôi" → db (D1/Neon/Turso › Supabase) └─ "Khăng khăng self-host tĩnh" → fs-build (ghi file + rebuild) ``` ## Tiếp theo [#tiếp-theo] # Envelope (CPP v1) (/docs/developer/protocol-envelope) 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 [#quy-tắc-bất-biến] 1. **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. 2. **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`, `status` là luôn có. 3. **`version`** = phiên bản envelope. CPP v1 dùng `version: 1`. Chỉ bump khi có *breaking* change (đổi nghĩa/xoá field) — thêm field KHÔNG bump. 4. **HTML sạch, JSON-LD tách riêng.** `html` là thân bài ngữ nghĩa, **không** chứa `