API อ่านข้อมูลสาธารณะ (v1)
ทุก endpoint ด้านล่างระบุไซต์จาก header Host และจากสิ่งนั้นเท่านั้น ไม่มีพารามิเตอร์ระบุไซต์ ไม่มี header ระบุไซต์ และผู้เรียกไม่มีทางเลือกไซต์ได้เลย — ซึ่งทำให้การอ่านข้ามไซต์เป็นไปไม่ได้ในเชิงโครงสร้าง แทนที่จะเป็นเพียงสิ่งที่ถูกห้าม
GET /health
หัวข้อที่มีชื่อว่า “GET /health”host นี้ให้บริการไซต์อยู่หรือไม่ ตอบเฉพาะ host ที่ deployment นี้รู้จักเท่านั้น host ที่ไม่รู้จักจะได้ 404 ที่นี่เหมือนกับทุกที่อื่นทุกประการ ดังนั้นจึงใช้ endpoint นี้ค้นหาว่ามีไซต์ใดอยู่บ้างไม่ได้
| Status | ความหมาย |
|---|---|
| 200 | host นี้ให้บริการไซต์ และ slug ของไซต์อยู่ใน body |
| 404 | ไม่มีไซต์ใดตอบบน host นี้ |
curl https://example.com/health{ "ok": true, "site": "acme" }GET /v1/:model
หัวข้อที่มีชื่อว่า “GET /v1/:model”แสดงรายการ item ที่เผยแพร่แล้วของโมเดลหนึ่ง เฉพาะ item ที่เผยแพร่แล้วเท่านั้น การเรียงลำดับ การกรอง และการแบ่งหน้าทั้งหมดเกิดขึ้นในฐานข้อมูล ดังนั้นหน้าหนึ่งจะไม่สั้นกว่า limit แบบเงียบ ๆ เพราะมีบางอย่างถูกตัดทิ้งหลัง query
| พารามิเตอร์ | จำเป็น | ความหมาย |
|---|---|---|
locale |
ไม่ | ภาษาที่จะให้บริการ ค่าเริ่มต้นคือภาษาเริ่มต้นของไซต์ การที่คำแปลที่ขาดหายจะ fallback หรือได้ 404 ขึ้นอยู่กับนโยบาย locale ของไซต์ (SITE) ไม่ใช่พารามิเตอร์นี้ — consumer ไม่สามารถ override ได้ โดยตั้งใจ |
limit |
ไม่ | ขนาดหน้า มีเพดานกำหนดโดยเซิร์ฟเวอร์ การขอมากกว่านั้นจะได้ค่าเพดาน |
cursor |
ไม่ | เป็นค่าทึบ (opaque) ให้นำมาจาก next_cursor และส่งกลับไปโดยไม่เปลี่ยนแปลง — เนื้อหาภายในไม่ใช่สัญญา และจะเปลี่ยนแปลง |
order |
ไม่ | asc, desc — จะอ่านจากปลายด้านใดของรายการ ตามวันที่เผยแพร่ desc คือใหม่สุดก่อนและเป็นค่าเริ่มต้น |
relation |
ไม่ | กรองตามฟิลด์ relation ในรูปแบบ relation=field:id ใส่ซ้ำได้ การใส่ซ้ำจะรวมกันแบบ AND ซึ่งเป็นการตีความเดียวที่ให้ consumer จำกัดผลให้แคบลงแทนที่จะขยายออก |
tag |
ไม่ | กรองตามฟิลด์ tag ในรูปแบบ tag=field:value ใส่ซ้ำได้ รวมแบบ AND คู่ relation และ tag รวมกันได้ไม่เกิน 16 คู่ต่อ request โดยนับตามจำนวนครั้งที่ใส่ซ้ำ: หากมากกว่านั้นจะถูกปฏิเสธด้วย 400 public_filter_too_many ซึ่งมี max แนบมาด้วย request จะไม่ถูกตัดให้แคบลงแบบเงียบ ๆ เพื่อให้พอดี |
| Status | ความหมาย |
|---|---|
| 200 | หน้าหนึ่งของ item ใน data พร้อม page.limit, page.has_more และ page.next_cursor อยู่ข้าง ๆ |
| 400 | พารามิเตอร์ที่ endpoint นี้ไม่รับ หรือค่าที่มีรูปแบบผิด |
| 404 | ไม่มีโมเดลนี้บนไซต์นี้, host นี้ไม่ได้ให้บริการไซต์ใด หรือ (OR) โมเดลเป็นแบบ private และไม่มี token ที่ถูกต้องส่งมา ใช้คำตอบเดียวสำหรับทุกกรณี โดยตั้งใจ: §7.1 ป้องกันไม่ให้พื้นผิวสาธารณะเปิดเผยโครงสร้างของไซต์ และ 401 จะยืนยันว่าโมเดลนั้นมีอยู่ |
curl 'https://example.com/v1/article?locale=th&limit=10&order=desc'{ "data": [ { "id": "ci_…", "slug": "hello", "locale": "th", "fields": { … } } ], "page": { "limit": 10, "has_more": true, "next_cursor": "…" } }GET /v1/:model/:slug
หัวข้อที่มีชื่อว่า “GET /v1/:model/:slug”อ่าน item ที่เผยแพร่แล้วหนึ่งรายการ หนึ่ง item ตาม slug slug ไม่ซ้ำกันภายในโมเดลหนึ่งบนไซต์หนึ่ง แต่ไม่ใช่ข้ามไซต์ — ไซต์ถูกกำหนดโดย host และไม่ใช่โดยสิ่งใดที่ผู้เรียกส่งมา
| พารามิเตอร์ | จำเป็น | ความหมาย |
|---|---|---|
locale |
ไม่ | ภาษาที่จะให้บริการ ค่าเริ่มต้นคือภาษาเริ่มต้นของไซต์ การที่คำแปลที่ขาดหายจะ fallback หรือได้ 404 ขึ้นอยู่กับนโยบาย locale ของไซต์ (SITE) ไม่ใช่พารามิเตอร์นี้ — consumer ไม่สามารถ override ได้ โดยตั้งใจ |
preview |
ไม่ | preview capability ที่ออกโดย CMS ให้บริการ DRAFT ของ item เพียงหนึ่ง (ONE) รายการ หมดอายุภายในไม่กี่นาที และไม่ถูก cache เด็ดขาด มันไม่ทำให้โมเดลแบบ private อ่านได้ และ preview ที่ถูกปฏิเสธจะตอบ 404 แทน 403 เพื่อให้การปฏิเสธไม่ยืนยันสิ่งใด |
| Status | ความหมาย |
|---|---|
| 200 | item นั้น ซึ่งถูก project เป็น locale ที่ร้องขอ |
| 400 | พารามิเตอร์ที่ endpoint นี้ไม่รับ |
| 404 | ไม่มี item นี้, ยังไม่เผยแพร่, ไม่มีเนื้อหาใน locale ที่นโยบายยอมให้บริการ, preview token ที่ตรวจสอบไม่ผ่าน หรือโมเดลแบบ private ที่ไม่มี token ที่ถูกต้อง ใช้คำตอบเดียวสำหรับทุกกรณี โดยตั้งใจ (§7.1) |
curl 'https://example.com/v1/article/hello?locale=th'{ "data": { "id": "ci_…", "slug": "hello", "locale": "th", "fields": { … } } }โมเดลแบบ private
หัวข้อที่มีชื่อว่า “โมเดลแบบ private”โมเดลที่ถูกกำหนดเป็น private จะอ่านได้เฉพาะเมื่อมี site API token ส่งมาในรูปแบบ
Authorization: Bearer dw_site_… token ถูกออกในหน้า admin ที่
Settings → Public API มี scope ที่ระบุว่าอ่านโมเดลใดได้บ้าง และ
แสดงเพียงครั้งเดียว
request ที่ไม่มี token และ request ที่มี token ผิด จะได้คำตอบที่เหมือนกัน (SAME) นั่น เป็นความตั้งใจ: การแยกแยะทั้งสองกรณีจะบอกผู้เรียกที่ถือค่าที่เดามาว่า โมเดลนั้นมีอยู่
Caching
หัวข้อที่มีชื่อว่า “Caching”โดยค่าเริ่มต้น API นี้ตอบด้วย no-store: ไม่มีสิ่งใดถูก cache ที่ edge และทุก
การอ่านไปถึง worker นี่คือค่าเริ่มต้นเดียวที่ถูกต้องเมื่อไม่มี purge
credential
Caching ต้องเปิดใช้เอง การตั้ง PUBLIC_CACHE_MAX_AGE เป็นจำนวนวินาทีที่เป็นบวก
— ทั้งบน public Worker และ admin Worker — จะเปิดใช้งาน: จากนั้น response
จะถูก cache และติด tag และการเผยแพร่ item หนึ่งจะ purge เฉพาะ response ที่
อ้างถึง item นั้นพอดี ต้องใช้ CF_PURGE_ZONE_ID และ CF_PURGE_API_TOKEN ที่มีสิทธิ์
Zone → Cache Purge หากไม่มี การเปลี่ยนแปลงจะรายงานว่าไม่สามารถล้าง
cache ได้ แทนที่จะซ่อนปัญหานั้นไว้
มีสองสิ่งที่ไม่ถูก cache เด็ดขาด ไม่ว่า TTL จะเป็นเท่าใด: response ของ request ที่มี site API token และ preview
สิ่งที่เสถียร และสิ่งที่ไม่เสถียร
หัวข้อที่มีชื่อว่า “สิ่งที่เสถียร และสิ่งที่ไม่เสถียร”เสถียร (STABLE) ภายใน v1: path, ชื่อพารามิเตอร์,
ความหมายของ status code และรูปร่างระดับบนสุดของ response — data
และบน route แบบรายการ จะมีออบเจกต์ page ที่เก็บ limit, has_more และ
next_cursor key ของ item เองคือ id, slug, model, locale,
version, published_at, fields, relations และ media
ไม่เสถียร (NOT stable) และห้าม parse: เนื้อหาของ next_cursor, ข้อความที่แน่นอนของ
error message และลำดับของ key พารามิเตอร์ทางเลือกใหม่หรือฟิลด์ใหม่
ภายใน item อาจปรากฏขึ้นภายใน v1 ได้ แต่ไม่มีสิ่งใดที่
บันทึกไว้ที่นี่แล้วจะเปลี่ยนความหมายโดยไม่มีเวอร์ชันใหม่ใน path
ตัวอย่างสำหรับ consumer
หัวข้อที่มีชื่อว่า “ตัวอย่างสำหรับ consumer”Fetch พร้อม cursor loop ที่เขียนอย่างถูกต้อง
หัวข้อที่มีชื่อว่า “Fetch พร้อม cursor loop ที่เขียนอย่างถูกต้อง”async function* everyArticle(origin, locale = 'en') { let cursor; do { const url = new URL(`${origin}/v1/article`); url.searchParams.set('locale', locale); url.searchParams.set('limit', '50'); // The cursor is opaque. Send back exactly what you were given; do not // decode it, and do not construct one. if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url); if (!response.ok) throw new Error(`${response.status} from ${url}`);
const body = await response.json(); yield* body.data; // Pagination lives in `page`, beside `data` rather than above it. cursor = body.page.has_more ? body.page.next_cursor : undefined; } while (cursor);}โมเดลแบบ private
หัวข้อที่มีชื่อว่า “โมเดลแบบ private”curl -H "Authorization: Bearer $DEE_WAN_SITE_TOKEN" \ 'https://example.com/v1/pricing'โมเดลแบบ private ที่ไม่มี token ที่ถูกต้องจะตอบ 404 ไม่ใช่ 401 — เป็นคำตอบเดียวกับ โมเดลที่ไม่มีอยู่ นั่นเป็นความตั้งใจ (§7.1): 401 จะยืนยันว่า โมเดลนั้นมีอยู่ และพื้นผิวสาธารณะไม่เปิดเผยสิ่งใดเกี่ยวกับโครงสร้างของไซต์ นั่น ยังหมายความว่า 404 ไม่สามารถตีความว่าเป็น “URL ผิด” ได้ ให้ตรวจสอบ token ก่อน
ใช้ TypeScript client ที่สร้างขึ้น
หัวข้อที่มีชื่อว่า “ใช้ TypeScript client ที่สร้างขึ้น”admin API ที่ต้องยืนยันตัวตนจะส่งออก client ที่ไม่มี dependency สำหรับไซต์ที่เลือก:
curl -H "X-Site-Id: $DEE_WAN_SITE_ID" -b "$DEE_WAN_SESSION_COOKIE" \ 'https://admin.example.com/api/models/public-client.ts' \ > src/dee-wan-public.tsไฟล์นี้ประกอบด้วย response envelope, locale union, type ของฟิลด์ที่แน่นอน, query type ที่รู้จักโมเดล และ method หนึ่งตัวต่อหนึ่งโมเดล ให้สร้างใหม่หลังจากโมเดลเปลี่ยน Dee Wan ไม่อัปเดต repository ของ consumer
หากคุณเรียก endpoint โดยไม่ใช้ client ที่สร้างขึ้น ให้กำหนด type ของ response envelope และฟิลด์ของโมเดล โดยตรง:
type Item<F> = { id: string; slug: string; locale: string; fields: F };type Page<F> = { data: Item<F>[]; page: { limit: number; has_more: boolean; next_cursor: string | null };};
type Article = { title: string; body: string };
const page: Page<Article> = await (await fetch(url)).json();