ข้ามไปยังเนื้อหา
Dee Wan 1.0 ยังอยู่ระหว่างพัฒนาและยังไม่เปิดตัว (pre-MVP) — ดู ขอบเขตผลิตภัณฑ์ 1.0

ข้อกำหนดข้อมูลหน้า (page contract)

หน้าที่ประกอบแล้วใน Dee Wan คือ content item ธรรมดาที่ model ของมันมีฟิลด์ json ชื่อ slides และฟิลด์ text ชื่อ title ฟิลด์ slides เก็บ เอกสารหน้า (page document): ข้อมูลแบบมีโครงสร้างที่ แสดงรายการ section ของหน้าตามลำดับ มันมีข้อความ ลิงก์ และการอ้างอิง media มันไม่มี HTML, CSS หรือ script เลย renderer และ theme ของคุณเป็นผู้ตัดสินว่าแต่ละ section หน้าตาเป็นอย่างไร

หน้านี้อธิบายเอกสารที่ renderer ได้รับจาก public read API และ วิธี parse มัน

อ่าน item ของหน้าจาก GET /v1/<model>/<slug>?locale=<locale> เอกสารอยู่ใน data.fields.slides และชื่อหน้าอยู่ใน data.fields.title แต่ละ locale มีเอกสารของตัวเอง ดังนั้นให้อ่าน locale ที่คุณกำลัง render

const res = await fetch("https://example.com/v1/page/about?locale=en");
if (!res.ok) throw new Error(`${res.status}`);
const { data } = await res.json();
const parsed = parsePublicRenderablePage(data.fields.slides);

item ยังมี data.page_contract.required ซึ่งเป็นเวอร์ชันข้อกำหนดต่ำสุดที่อ่านหน้านี้ได้ ดู ความเข้ากันได้

ข้อกำหนดนี้เป็น TypeScript module ขนาดเล็กที่ไม่มี dependency มันไม่ import อะไรนอกไดเรกทอรี ของตัวเอง จึงรันได้ใน Cloudflare Workers, ในเบราว์เซอร์ และใน Node

  • ไซต์ที่สร้างขึ้น มีมันอยู่แล้ว คัดลอกมาโดยไม่เปลี่ยนแปลง ที่ src/lib/page-contract/
  • กรณีอื่น มันอยู่ใน repository ของ Dee Wan ที่ backend/src/lib/page-contract/ ไม่มี npm package ที่เผยแพร่ ให้คัดลอกทั้งไดเรกทอรีจาก Dee Wan เวอร์ชันที่คุณรัน และแทนที่มัน เมื่ออัปเกรด อย่าพิมพ์ type ใหม่ด้วยมือ
Function คืนค่า
parsePublicRenderablePage(raw) { sections }
parsePublicPageDocument(raw) เอกสารที่ parse แล้ว

ใช้ parsePublicRenderablePage function นี้แปลงหน้าที่เก็บไว้ให้เป็นรายการส่วนที่เรียงลำดับแล้วเพียงรายการเดียว

parser แต่ละตัวรับอาร์กิวเมนต์เดียว คือค่าดิบของ fields.slides (object หรือ JSON string) มัน ไม่ต้องใช้ฐานข้อมูลและไม่เรียกเครือข่าย

parser ทุกตัวคืนผลลัพธ์เสมอโดยไม่โยนข้อผิดพลาด:

type Parsed<T> =
| { ok: true; value: T }
| {
ok: false;
error: { code: string; message: string; detail: Record<string, unknown> };
};

เมื่อหน้าถูกปฏิเสธ อย่า render บางส่วนของมัน ให้ถือว่าเป็นข้อผิดพลาดสำหรับ URL นั้น

PAGE_FORMATS คือ [2, 3] เอกสารใช้ format: 2 ตามปกติ และใช้ format: 3 เมื่อมีส่วนอย่างน้อยหนึ่งส่วนที่ตรึงคอมโพเนนต์ใช้ซ้ำไว้ (ดู คอมโพเนนต์ใช้ซ้ำ) format ที่หายไปหรือค่าอื่นใดจะถูกปฏิเสธด้วย unreadable_page เอกสารที่ว่างหรือหายไปก็ถูกปฏิเสธด้วย unreadable_page เช่นกัน ดังนั้น item ที่ไม่มีเนื้อหาหน้าจะไม่ถูก render เป็นหน้าว่าง

format 2 ไม่เปลี่ยน: component ของมันเป็น null เท่านั้น เอกสาร format 3 ที่ไม่ปักคอมโพเนนต์ใดเลย จะถูกปฏิเสธด้วย noncanonical_default ดังนั้นหนึ่งหน้ามีการเขียนได้แบบเดียว

format บอกว่าหน้าถูกเก็บอย่างไร มันไม่ใช่เวอร์ชันของข้อกำหนด ซึ่งระบุว่าตัวอ่านเอกสารต้องรองรับคำศัพท์ชุดใด (ดู ความเข้ากันได้)

แต่ละ locale เก็บเอกสารของตัวเอง โครงสร้าง (section ใดบ้าง ลำดับใด ใช้ variant และ layout ใด) เป็นของ locale นี้ และสอง locale ของหน้าเดียวกันอาจต่างกันได้ในทุกส่วน เนื้อหาสำหรับ locale นี้อยู่ใน props โดยใช้ id ของ section เป็นคีย์

{
"format": 2,
"sections": [
{
"id": "intro",
"type": "hero",
"variant": "split",
"layout": "default",
"component": null
},
{
"id": "story",
"type": "text_media",
"variant": "balanced",
"layout": "media_side=start",
"component": null
}
],
"props": {
"intro": {
"heading": "Fresh cakes daily",
"subheading": null,
"media": null,
"link": { "label": "See the menu", "href": "/menu" }
},
"story": {
"heading": "Our story",
"paragraphs": ["We started in 2019."],
"media": null
}
}
}

กฎ:

  • ทุก section ต้องมี props และทุกรายการใน props ต้องเป็นของ section ใด section หนึ่ง
  • ต้องมี component ในหน้า format 2 เป็น null ในหน้า format 3 เป็น null หรือ ใน section custom เป็นการปักคอมโพเนนต์
  • props ต้องไม่ซ้ำ id, type หรือคีย์ของ layout เช่น media_side สิ่งเหล่านั้นเป็นของ โครงสร้าง
  • section อาจมี appearance ในโครงสร้าง มันไม่บังคับ และเป็นของ locale นี้ เหมือน variant ดู รูปลักษณ์ของ section

parser คืนข้อมูลในรูปแบบเดียวกัน:

type SectionType =
| "hero"
| "rich_text"
| "text_media"
| "feature_grid"
| "cta"
| "cards"
| "stats"
| "faq"
| "testimonials"
| "media"
| "logos"
| "steps"
| "pricing"
| "menu"
| "contact"
| "latest_entries"
| "form"
| "custom"
| "search";
type RenderablePage = {
sections: Array<{
id: string;
type: SectionType;
variant: string;
layout: string;
component: {
component_id: string;
version_id: string;
artifact_digest: string;
} | null;
appearance?: {
background?: "muted" | "inverse" | "accent";
spacing?: "compact" | "roomy";
motion?: "reveal" | "parallax";
};
/** `custom` sections only: the registered section version this section pins. */
definition?: { key: string; version: number; digest: string };
props: SectionPropsMap[SectionType];
}>;
};

type จริงเป็น union ที่ใช้ type เป็นคีย์ ดังนั้นการ switch บน section.type จะจำกัดชนิดของ props ให้ตรงกับส่วนชนิดนั้น SectionPropsMap ระบุ props ของแต่ละ type

ค่าที่ไม่บังคับจะคืนมาเป็น null เมื่อไม่มี ยกเว้นฟิลด์ที่เพิ่มภายหลัง: ฟิลด์เหล่านั้นจะไม่ปรากฏเลย ข้อความถูก trim ยกเว้นข้อความของ run ที่จัดรูปแบบ ทุกรายการ ในลิสต์เป็น object แบบปิด คีย์ที่ไม่รู้จักจะถูกปฏิเสธ

Type Props
hero heading (บังคับ), subheading, media, link เพิ่มภายหลัง: eyebrow, secondary_link
rich_text heading, paragraphs (บังคับ, 1–12)
text_media heading, paragraphs (บังคับ, 1–12), media
feature_grid heading, features (บังคับ, 1–12 รายการของ { title, body })
cta heading (บังคับ), body, link (บังคับ) เพิ่มภายหลัง: secondary_link
cards heading, items (บังคับ, 1–12 รายการของ { title, body, media, link } title บังคับ)
stats heading, items (บังคับ, 1–8 รายการของ { value, label, note } value และ label บังคับ)
faq heading, items (บังคับ, 1–20 รายการของ { question, answer } บังคับทั้งคู่)
testimonials heading, items (บังคับ, 1–12 รายการของ { quote, name, role, media } quote และ name บังคับ)
media heading, items (บังคับ, 1–24 รายการที่เป็นภาพหรือวิดีโอ)
logos heading, items (บังคับ, 1–24 รายการของ { media, name, link } media และ name บังคับ)
steps heading, items (บังคับ, 1–12 รายการของ { title, body } title บังคับ)
pricing heading, plans (บังคับ, 1–4 รายการของ { name, price, period, description, features, link, highlighted } name และ price บังคับ)
menu heading, groups (บังคับ, 1–8 รายการของ { title, items } items มี 1–30 รายการของ { name, description, price }, name บังคับ)
contact heading, body, address, hours (0–14 รายการของ { label, value }), phone, email, map_link, link
latest_entries heading, model (บังคับ), title_field (บังคับ), cover_field, summary_field, date_field, limit (บังคับ, จำนวนเต็ม 1–12), empty_text (ไม่เกิน 200 ตัวอักษร)
form heading, body, fields (บังคับ, 1–12 รายการแบบ { name, kind, label, required, help, options }), consent, submit_label (บังคับ), success_text (บังคับ)
custom heading, fields (ค่าของ section ที่ลงทะเบียนซึ่งโครงสร้างปักไว้)
search heading, placeholder (ไม่เกิน 80 ตัวอักษร)
shared ไม่มี โครงสร้างระบุ shared section (shared.entry) renderer ไม่เคยได้รับมัน

ลิงก์คือ { label, href } การอ้างอิง media คือ { media_id, url, alt } โดย alt อาจเป็น null รายการในลิสต์ก็มี media ได้: media ในรายการของ cards, testimonials และ logos และ media หรือ poster ในรายการของ media

การอ้างอิง media ยังเก็บการจัดกรอบภาพของมันได้:

  • crop: { x, y, width, height } เป็นสัดส่วนของภาพที่เก็บไว้ตั้งแต่ 0 ถึง 1 ความกว้างและความสูง มากกว่า 0 และสี่เหลี่ยมอยู่ภายในภาพ
  • focal: { x, y } เป็นสัดส่วนตั้งแต่ 0 ถึง 1 คือจุดที่ต้องอยู่ในกรอบเมื่อกล่องตัดภาพ

แต่ละ key มีอยู่เฉพาะเมื่อกำหนดไว้ และไม่เคยเป็น null รูปแบบอื่นจะถูกปฏิเสธ การจัดกรอบเป็นของการใช้ ภาพครั้งนี้ครั้งเดียว ภาพเดียวกันจึงจัดกรอบต่างกันได้ในสอง section ภาพปกในคำตอบของรายการ (EntryMedia) ก็พกการจัดกรอบของ entry แบบเดียวกัน

ฟิลด์ ความหมาย
hero.eyebrow บรรทัดสั้น ๆ เหนือ heading
hero.secondary_link ลิงก์ที่สอง ใช้ได้เฉพาะเมื่อมี link
cta.secondary_link ลิงก์ที่สอง

ฟิลด์เหล่านี้จะไม่ถูกเขียนเมื่อไม่ได้ใช้ ไม่เป็น null หรือค่าว่าง หลัง parse ฟิลด์ที่ไม่ได้ใช้ จะไม่มีอยู่ (undefined) การอ่านแบบ public จะปฏิเสธ null หรือ "" ที่เขียนไว้ชัดเจนด้วย noncanonical_default

  • media: แต่ละรายการคือ { kind: "image", media, caption } หรือ { kind: "video", provider, video_id, title, poster, caption } รายการภาพต้องมี media รายการวิดีโอต้องมี provider, video_id และ title ดู วิดีโอ
  • pricing: features คือลิสต์ของข้อความ 0–12 รายการ ถ้าไม่มีจะอ่านเป็น [] highlighted เป็น true หรือไม่มี และใน section หนึ่งเน้นได้ไม่เกินหนึ่งแผน false จะถูกปฏิเสธด้วย noncanonical_default
  • contact: ต้องมีอย่างน้อยหนึ่งอย่างจาก address, phone, email, map_link หรือรายการ hours หนึ่งรายการ ถ้าไม่มี hours จะอ่านเป็น [] phone มีตัวเลข ช่องว่าง (, ), . และ - โดยอาจมี + นำหน้า ยาวอย่างน้อย 3 ตัวอักษร email คือที่อยู่เดียว ไม่ใช่ลิงก์ mailto: map_link.href ต้องขึ้นต้นด้วย https://
  • logos: ทุกรายการต้องมีภาพ

section นี้เก็บการแมป query ไม่เคยเก็บ entry หรือ cursor model และทุก *_field เป็น slug (^[a-z][a-z0-9_-]{0,63}$) renderer resolve มันตอน request ผ่าน public list API ของไซต์ GET /v1/<model> โดยเลือกเฉพาะฟิลด์ที่แมปไว้: ใหม่สุดก่อนตาม date_field เมื่อตั้งไว้ (entry ที่ไม่มีค่านี้อยู่ท้ายสุด) มิฉะนั้นเรียงจากที่เผยแพร่ล่าสุดก่อน ไม่เกิน limit รายการ หากไม่มีโมเดลหรือฟิลด์ดังกล่าว หาก query ถูกปฏิเสธ หรือหากอ่านคำตอบไม่ได้ จะถือเป็นข้อผิดพลาดในการ render ของ section นั้น ไม่ใช่ลิสต์ว่าง มีเพียงคำตอบที่ถูกต้องซึ่งไม่มี entry เลยที่แสดง empty_text หรือข้อความกลาง ๆ summary จากฟิลด์ rich text จะถูก render ด้วย renderRichDocument (rich-document.ts) ก่อนถูกแทรก ส่ง origin ของการฝังที่ไซต์อนุญาต (GET /v1/_embeds) เป็น embedProviders หากไม่ส่ง การฝังจาก origin ที่ตั้งค่าไว้จะแสดงเป็นลิงก์ธรรมดา

collection.ts เก็บข้อกำหนด query ร่วมที่ renderer ทุกตัวของ Dee Wan ใช้: entriesQuery, readEntriesPage, resolveLatestEntries, walkToPage และรหัสการปฏิเสธใน COLLECTION_REFUSALS ดูเอดิเตอร์ได้ที่ เว็บไซต์ Blog

ฟอร์มระบุฟิลด์เท่านั้น ไม่เคยระบุปลายทาง ผู้รับ หรือ secret ปลายทางของการส่งเป็นการตั้งค่าของไซต์ (การตั้งค่า → การเชื่อมต่อ) และผู้เยี่ยมชมเลือกเองไม่ได้

  • แต่ละฟิลด์คือ { name, kind, label, required, help, options } ทุกคีย์ต้องระบุ
  • name คืออักษรตัวเล็ก ตัวเลข หรือ _ 1–32 ตัว ขึ้นต้นด้วยตัวอักษร และไม่ซ้ำกันในฟอร์ม
  • kind คือ text, email, phone, textarea, select, checkbox, number, date หรือ file
  • number รับเลขฐานสิบที่มีค่าจำกัด รวมถึงรูปแบบเลขยกกำลัง และบันทึกเป็นตัวเลข JSON ส่วน date รับวันที่ที่มีอยู่จริงตามปฏิทินในรูปแบบ YYYY-MM-DD ปี 0001–9999 โดยไม่แปลงเขตเวลา หากฟิลด์ไม่บังคับและเว้นว่าง จะเก็บเป็น "" ส่วนฟิลด์บังคับจะเว้นว่างไม่ได้
  • options มี 2–20 ตัวเลือกสำหรับ select และเป็น [] สำหรับชนิดอื่น
  • ฟิลด์ file กำหนดเพิ่มได้อีกคือ accept (MIME type แบบเจาะจงหรือนามสกุลสูงสุด 10 รายการ), max_files (1–5 ค่าเริ่มต้น 1) และ max_bytes (1–5242880 ค่าเริ่มต้น 5242880) ชนิดอื่นไม่รับคีย์เหล่านี้ accept ว่างจะรับประเภทภาพและเอกสารใน FORM_FILE_TYPES ไม่รับไวลด์การ์ด ข้อมูลหนึ่งรายการมีไฟล์ได้ ไม่เกิน 5 ไฟล์ รวมไม่เกิน 10485760 ไบต์ และเนื้อหาไฟล์ต้องตรงกับรูปแบบที่รองรับ
  • consent คือข้อความยินยอมที่ผู้เยี่ยมชมต้องยอมรับ หรือ null ถ้าไม่มีช่องยินยอม

forms.ts คือทะเบียน interaction และข้อกำหนดการส่งที่ renderer ทุกตัวและ intake ใช้ร่วมกัน:

Export คืออะไร
INTERACTIONS, FORM_SUBMIT_PATH action ที่ลงทะเบียนไว้ (registry เวอร์ชัน 3): form.submit ส่งไปที่ /v1/interactions/form.submit, comment.submit ส่งไปที่ /v1/interactions/comment.submit และ comment.react ส่งไปที่ /v1/interactions/comment.react
readSubmission(raw) ซองการส่งแบบปิด: { action, page, locale, section, key, values, consent, website } อย่างอื่นคือ null
checkSubmission(form, values, consent, files?) ตรวจค่าเทียบกับฟอร์มที่เผยแพร่ พร้อม FieldError สำหรับแต่ละปัญหา
FORM_STATES, FORM_STATE_COPY, FIELD_PROBLEM_COPY ทุกสถานะที่ฟอร์มวาด และถ้อยคำของแต่ละสถานะ
readSubmissionAnswer(raw), stateOfAnswer(answer) คำตอบ JSON ของ intake และสถานะที่ตามมา
formViews(sections, context), readFormResult(query, section) สิ่งที่ renderer ส่งให้ component ฟอร์ม รวมถึงผลลัพธ์ที่อ่านกลับจากที่อยู่ของหน้า

ฟอร์มที่มีฟิลด์ไฟล์ส่ง multipart/form-data ค่าทั่วไปใช้ values.<name> ช่องทำเครื่องหมายใช้ checks.<name> พร้อมค่า true เมื่อเลือก ส่วนไฟล์ใช้ส่วน files.<name> ซ้ำได้หลายส่วน action, page, locale, section, key, consent และ website ยังคงเป็นส่วนข้อความ ซอง JSON ของฟอร์มที่ไม่มีไฟล์ไม่เปลี่ยนแปลง ไฟล์ไม่ใช่ค่าข้อความหรือบูลีนใน JSON ขนาด multipart จำกัดไฟล์รวม 10 MiB บวกส่วนประกอบคำขอ 64 KiB โดยส่วนข้อความรวมไม่เกิน 16 KiB การส่งซ้ำต้องมีชื่อ ประเภท ขนาด และไบต์ของไฟล์เดิม จึงจะได้บันทึกการรับข้อมูลเดิม หากเปลี่ยนไฟล์แต่ใช้คีย์เดิม ระบบจะปฏิเสธ

รายละเอียดที่ยังเก็บไว้อาจมี attachments: Record<fieldName, Array<{ id, name, type, size }>> GET /api/forms/submissions/:id/attachments/:attachmentId ส่งเนื้อไฟล์ส่วนตัวโดยตรวจสิทธิ์ site:infra ของเว็บไซต์ที่เลือก ไฟล์ที่หมดอายุ ถูกลบ ไม่มีอยู่ หรือเป็นของเว็บไซต์อื่นจะดาวน์โหลดไม่ได้ ไม่เปิดเผยคีย์ที่เก็บไฟล์หรือ URL สาธารณะ ดู ไฟล์แนบ

website คือกับดักบอต คนจะไม่เห็นช่องนี้ การส่งที่มีค่าในช่องนี้จะได้คำตอบว่ารับแล้วแต่ไม่ถูกเก็บ key (ตัวอักษร ตัวเลข _ หรือ - 16–64 ตัว) ทำให้การลองซ้ำเป็นการส่งเดียวกัน key เดิมกับค่าเดิม จะได้ใบรับเดิม ส่วน key เดิมกับค่าต่างกันจะถูกปฏิเสธ

หน้าไม่เคยเก็บ markup ของฟอร์ม renderer วาดฟอร์มด้วย component ของแพลตฟอร์ม: ใน managed renderer คือ src/runtime/Form.astro และในไซต์ที่ export คือ src/components/FormRuntime.astro template วาง component นี้ได้ แต่เขียน <form> หรือ <script> เองไม่ได้

comments.ts คือข้อกำหนดความคิดเห็นของทุกโมเดลที่รับความคิดเห็น: readCommentSubmission และ checkComment สำหรับความคิดเห็น readReactionSubmission และ readReactionAnswer สำหรับ reaction Helpful readCommentsPage และ walkComments สำหรับความคิดเห็นที่อนุมัติแล้วซึ่ง GET /v1/_comments ส่งให้ตามลำดับหนึ่งใน COMMENT_ORDERS (oldest, newest, best) และ commentsView สำหรับสิ่งที่หน้ารายการวาด: เธรดพร้อมจำนวน reaction ลิงก์เปลี่ยนลำดับ การแบ่งหน้า ฟอร์ม และผลลัพธ์ที่อ่านกลับจากที่อยู่ renderer ทั้งสองวาดด้วย component ของแพลตฟอร์ม: src/runtime/Comments.astro ใน managed renderer (โพสต์บล็อกและหน้ารายการที่มี route) และ src/components/Comments.astro ในไซต์ที่ export (โพสต์บล็อกและหน้ารายการของโมเดลที่รับความคิดเห็น) ไม่มีอีเมล สถานะการดูแล หรือผู้กด reaction ดู ความคิดเห็นของผู้เยี่ยมชม

นักพัฒนาลงทะเบียน section แบบมี type ได้ผ่าน managed template นิยามและ component Astro ถูก build ใน sandbox แบบออฟไลน์ที่ปักเวอร์ชันไว้ และการยอมรับ template ด้วยสิทธิ์ site:deploy จะ promote เป็นเวอร์ชันที่แก้ไขไม่ได้ หน้าจะปักเวอร์ชันนั้นไว้ในโครงสร้างของ section:

{
"id": "sl_team",
"type": "custom",
"variant": "grid",
"layout": "default",
"component": null,
"definition": {
"key": "team-grid",
"version": 2,
"digest": "<64 hex characters>"
}
}
  • definition บังคับสำหรับ section custom และถูกปฏิเสธใน type อื่น ในหน้า format 2 component ยังเป็น null format นั้นไม่ตีความใหม่
  • digest คือ SHA-256 ของ canonical JSON { definition, source }: นิยามที่นักพัฒนาประกาศและไบต์ของ component ตรงตามจริง renderer ไม่เคยเลือกเวอร์ชันล่าสุดจาก key
  • props ของ locale คือ { heading, fields } fields เก็บค่าของนิยาม: ข้อความ ค่าเปิดปิด ลิงก์ การอ้างอิง media, null หรือลิสต์ของรายการที่เก็บค่าเหล่านี้
  • ถ้าไม่มีนิยาม parser ตรวจ fields ตามไวยากรณ์ที่มีขอบเขตนี้เท่านั้น: markup ลิงก์ที่ไม่ปลอดภัย และ media ที่ผิดรูปแบบยังถูกปฏิเสธเหมือนเดิม ถ้ามี (ParseContext.definitions) ทุกค่าจะถูกตรวจตามนิยามที่ ปักไว้ และการปักที่ไม่รู้จักคือ unknown_section_definition
  • sections.ts อ่านนิยาม (parseSectionDefinitionSource, parseSectionDefinition) จัดประเภทการ เปลี่ยนแปลงระหว่างสองเวอร์ชัน (definitionChange: ไม่เปลี่ยน เข้ากันได้ หรือไม่เข้ากัน พร้อมเหตุผล) และย้ายเนื้อหาไปเวอร์ชันใหม่ (migrateCustomFields) โดยเก็บเฉพาะค่าที่เวอร์ชันใหม่ยังรับได้

renderer ที่ไม่มี component สำหรับที่อยู่ที่ปักไว้จะปฏิเสธทั้งหน้า ไซต์ที่ export จะพก component ของทุกเวอร์ชันที่ปักไว้ใน src/registered/<digest>/ และระบบจะปฏิเสธบิลด์หากมีหน้าที่ตรึงเวอร์ชันซึ่งไม่สามารถรวมไว้ในบิลด์นั้นได้

คอมโพเนนต์ใช้ซ้ำคือ section ที่ลงทะเบียนซึ่งสร้างจาก section บนหน้า: สร้างโดย AI, build ใน sandbox แบบออฟไลน์ที่ปักเวอร์ชันไว้, ตรวจสอบ และอนุมัติด้วยสิทธิ์ site:deploy (ดู สร้างและแก้ไขหน้า) หน้าที่ใช้มันถูกเก็บใน format 3 และ section custom ปักเวอร์ชันที่แน่นอนไว้ข้างนิยามของตัวเอง:

{
"id": "sl_team",
"type": "custom",
"variant": "grid",
"layout": "default",
"component": {
"component_id": "rcp_…",
"version_id": "rcv_…",
"artifact_digest": "<64 hex characters>"
},
"definition": {
"key": "team-grid",
"version": 2,
"digest": "<64 hex characters>"
}
}
  • component ยอมรับเฉพาะในหน้า format 3 และเฉพาะ section custom ที่มี definition ด้วย ชื่อเปล่าหรือ slug ถูกปฏิเสธ (invalid_slide): ชื่อไม่ได้บอกว่าหมายถึงโค้ดใด
  • component_id ระบุคอมโพเนนต์ของไซต์ version_id ระบุเวอร์ชันที่เปลี่ยนแปลงไม่ได้ และ artifact_digest คือ SHA-256 ของ build ที่คอมไพล์แล้วซึ่งเวอร์ชันนั้นถูกอนุมัติมา ที่อยู่ของ definition คือที่อยู่ที่เวอร์ชันนั้นชี้ไป renderer จึงวาดตาม digest เหมือน section ที่ลงทะเบียนทั่วไป
  • ตัวอ่านข้อมูลที่มีคอมโพเนนต์ของไซต์ (ParseContext.components) ตรวจว่าเวอร์ชันผ่านการอนุมัติแล้วและเวอร์ชันที่ตรึงไว้ตรงกับนิยาม มิฉะนั้นจะปฏิเสธด้วย unknown_component_version การ parse คำขอโดยไม่มีตัวอ่านข้อมูลดังกล่าวจะปฏิเสธการตรึงเวอร์ชันทั้งหมด จึงไม่สร้างการตรึงเวอร์ชันจากข้อมูลที่ไซต์ตรวจสอบไม่ได้
  • ไม่มีอะไรเปลี่ยนการปักเป็นเวอร์ชันใหม่กว่า ไม่ว่าตอนวาด บันทึก หรือกู้คืน เวอร์ชันใหม่กว่าคือการ เปลี่ยนการปักอย่างชัดเจน
  • หน้าที่ไม่มีการปักยังอยู่ใน format 2 และ storedPage เขียน format ที่เนื้อหาต้องการ

ช่องค้นหาของไซต์เอง หน้าเก็บเฉพาะถ้อยคำ: heading และ placeholder ซึ่งไม่บังคับทั้งคู่ หน้าไม่ระบุ index, endpoint หรือคำค้น ผลลัพธ์มาจากการค้นหาของไซต์ ในภาษาของหน้า

  • editor เสนอ section นี้เฉพาะเมื่อเปิดการค้นหาของไซต์ (GET /api/pages/renderer-capability → site_search.active) ภายใต้ renderer ทุกแบบ
  • managed release วาดมันด้วย component ค้นหาของแพลตฟอร์มเอง ไม่ว่า template จะเป็นแบบใด (PLATFORM_DRAWN_TYPES) ฟอร์มส่ง q ไปยังหน้าเดิม ซึ่งอ่าน GET /v1/_search และแสดงผลลัพธ์ในที่เดิม template ไม่ต้องกำหนด component ให้
  • ไซต์ที่ export วาดมันด้วย component SiteSearch ของตัวเอง หน้าที่มี section นี้ render ทุกคำขอ และอ่าน GET /v1/_search บนโฮสต์ของไซต์เอง ไซต์ที่ build ขณะปิดการค้นหาจะไม่ยอม build หน้าที่มี section นี้
  • renderer ทั้งสองเพิ่มคำแนะนำขณะผู้เข้าชมพิมพ์: ฟอร์มอ่าน /search?suggest= ของไซต์ตัวเอง และยังทำงานเป็นฟอร์ม GET ธรรมดาได้เมื่อไม่มี JavaScript
  • ขณะปิดการค้นหา รายการคุณภาพใน editor จะไม่ให้บันทึกหน้าที่มี section นี้

ข้อกำหนดเวอร์ชัน 5 shared section คือเนื้อหาที่ไซต์เก็บไว้ที่เดียวและแสดงบนหลายหน้า: ประกาศ โปรโมชัน เนื้อหาส่วนท้าย หรือ call to action มันเป็น item ธรรมดาของ model ที่สงวนไว้ shared_section (มี title และ slides เหมือนหน้า) ซึ่งแก้ไข เก็บเวอร์ชัน และเผยแพร่เหมือนหน้า

หน้าเก็บการอ้างอิง ไม่ใช่สำเนา:

{
"id": "notice",
"type": "shared",
"variant": "default",
"layout": "default",
"component": null,
"shared": { "entry": "ci_…" }
}

props ของมันคือ {} shared.entry คือ id ของ item เจ้าของ shared section แต่ละ locale มีการอ้างอิงของตัวเอง variant และ layout เป็น default และไม่มี appearance

  • การส่งมอบจะขยายมัน การอ่านสาธารณะทุกแบบ (/v1/:model, /v1/:model/:slug, preview), managed working render, preview ของ template และไซต์ที่ export จะแทนที่การอ้างอิงด้วย section ที่เผยแพร่แล้ว (PUBLISHED) ของ shared section ในภาษาของหน้า (ใช้ลำดับภาษาสำรองของไซต์) id ของ section กลายเป็น <reference id>.<n> ร่างจะไม่แสดงที่ใดเลย shared section ที่ไม่มีอะไรเผยแพร่ในภาษานั้นจะไม่แสดงอะไร renderer จึงไม่เคยได้รับ type: "shared": parsePublicRenderablePage ปฏิเสธด้วย shared_section_unexpanded
  • Cache response ที่ฝัง shared section มี instance:<shared section id> ซึ่งเป็น tag ที่การเผยแพร่ shared section นั้นล้าง
  • สิ่งที่ shared section เก็บได้ section ได้สูงสุด SHARED_SECTION_MAX (6) ห้ามมี shared section อื่น (shared_section_nested) และห้ามมี section form, search หรือ custom (shared_section_type_unsupported) section มากเกินไปคือ shared_section_too_many การบันทึกปฏิเสธทุกกรณีเหล่านี้
  • การเผยแพร่หน้า ตรวจเฉพาะภาษาที่กำลังเผยแพร่ หากส่วนที่ใช้ร่วมกันไม่มีภาษาใดเผยแพร่อยู่ จะปฏิเสธด้วย shared_section_not_live หากจำนวนส่วนหลังขยายเกิน LIMITS.slides จะปฏิเสธด้วย shared_section_page_limit การเผยแพร่ส่วนที่ใช้ร่วมกันปฏิเสธด้วย shared_section_page_limit หากเวอร์ชันที่เผยแพร่ใหม่จะทำให้หน้าที่เผยแพร่อยู่ซึ่งใช้ส่วนนี้เกินขีดจำกัด ระบบตรวจเวอร์ชันที่เผยแพร่ที่เกี่ยวข้องและชุดหน้าที่ใช้อีกครั้งในชุดคำสั่งเดียวกับที่บันทึกการเผยแพร่ หากมีการเปลี่ยนแปลงพร้อมกัน จะปฏิเสธด้วย 409 shared_section_publication_changed ให้ลองเผยแพร่อีกครั้งโดยใช้เนื้อหาปัจจุบัน การนำเข้าสร้างสถานะเดิมของไซต์ที่ทั้งหน้าและส่วนที่ใช้ร่วมกันเผยแพร่อยู่แล้ว จึงข้ามการตรวจข้อแรก
  • การถอนออก การยกเลิกเผยแพร่ shared section จะแสดงทุกหน้าที่ใช้มันในการยืนยันรายการที่ขึ้นกับมัน (field_keys: ["slides"]) การลบต้องยกเลิกเผยแพร่ก่อน หลังจากนั้นแต่ละหน้าจะไม่แสดงอะไรในตำแหน่งนั้น
  • ไม่ใช่หน้า ไม่มีที่อยู่ ไม่มี route (shared_section_not_routable ใน route map) ไม่มีรายการใน sitemap และไม่มีรายการในการค้นหา
  • pattern ที่บันทึกไว้ ยังเป็นสำเนา: pattern ปฏิเสธ shared section (pattern_shared_section)
  • ทุกเวอร์ชันบันทึก shared section ที่ใช้ไว้ใน meta.shared_sections ซึ่ง SQL อ่านได้แม้ snapshot จะถูกบีบอัด
Type Variant Variant เริ่มต้น Layout
hero centred, split, full_bleed centred default
rich_text single_column, wide, narrow, statement single_column default
text_media balanced, media_dominant, text_dominant balanced media_side=start, media_side=end
feature_grid two_column, three_column, list three_column default
cta banner, card, inline banner default
cards grid, bento, scroller grid default
stats row, grid, spotlight row default
faq accordion, list, split accordion default
testimonials spotlight, grid, scroller spotlight default
media full_width, grid, masonry, scroller full_width default
logos grid, row, marquee grid default
steps numbered, timeline, columns numbered default
pricing cards, compact, stacked cards default
menu list, two_column, cards list default
contact split, card, compact split default
latest_entries grid, list grid default
form stacked, split, card stacked default
custom variant ของนิยามที่ปักไว้ ตัวแรกของมัน default
search default default default

ค่าคงที่คือ SECTION_VISUAL_VARIANTS, DEFAULT_VISUAL_VARIANT และ SECTION_LAYOUTS

section อาจมี appearance อยู่ข้าง variant ของมัน:

{
"id": "story",
"type": "rich_text",
"variant": "wide",
"layout": "default",
"component": null,
"appearance": { "background": "muted", "motion": "reveal" }
}
คีย์ ค่า เมื่อไม่มีคีย์
background muted, inverse, accent พื้นหลังเริ่มต้น
spacing compact, roomy ระยะห่างปกติ
motion reveal, parallax ไม่มีการเคลื่อนไหว
  • แต่ละค่าเป็นเพียงชื่อ ส่วนธีมของคุณเป็นตัวกำหนดรูปลักษณ์
  • คีย์ที่เป็นค่าเริ่มต้นจะไม่ถูกเขียน section ที่ไม่ได้ตั้งคีย์ใดเลยจะไม่มี appearance เลย object ว่างจะถูกปฏิเสธด้วย noncanonical_default
  • คีย์หรือค่าอื่นใดจะถูกปฏิเสธด้วย unknown_section_appearance
  • ปิดการเคลื่อนไหวทั้งหมดเมื่อผู้เข้าชมขอให้ลดการเคลื่อนไหว (prefers-reduced-motion: reduce)

ค่าคงที่คือ SECTION_APPEARANCE

อะไร สูงสุด
slide หรือ section ต่อหน้า 24
ย่อหน้าต่อ section 12
run ที่จัดรูปแบบต่อย่อหน้า 32
feature ต่อ grid 12
card, testimonial หรือ step ต่อ section 12
stat ต่อ section 8
คำถาม FAQ ต่อ section 20
รายการ media หรือ logo ต่อ section 24
แผนราคาต่อ section 4
feature ต่อแผนราคา 12
กลุ่มเมนูต่อ section 8
รายการต่อกลุ่มเมนู 30
รายการเวลาทำการ 14
Heading 200 ตัวอักษร
ย่อหน้า, subheading, body ของ CTA หรือ contact 2000 ตัวอักษร
Eyebrow 80 ตัวอักษร
label ของลิงก์ 80 ตัวอักษร
URL ของลิงก์หรือ media 2000 ตัวอักษร
title ของ feature 120 ตัวอักษร
body ของ feature 600 ตัวอักษร
title ของ card หรือ step, title ของกลุ่มเมนู 120 ตัวอักษร
body ของ card หรือ step 600 ตัวอักษร
value ของ stat 24 ตัวอักษร
label ของ stat 80 ตัวอักษร
note ของ stat 200 ตัวอักษร
คำถาม FAQ 200 ตัวอักษร
คำตอบ FAQ 2000 ตัวอักษร
quote ของ testimonial 600 ตัวอักษร
ชื่อบุคคล 80 ตัวอักษร
ตำแหน่งของบุคคล 120 ตัวอักษร
caption หรือชื่อวิดีโอ 200 ตัวอักษร
ชื่อ logo 80 ตัวอักษร
ชื่อแผนราคา 80 ตัวอักษร
ราคาหรือรอบเวลา (period) 24 ตัวอักษร
คำอธิบายแผนราคา 300 ตัวอักษร
feature ของแผนราคา 120 ตัวอักษร
ชื่อรายการเมนู 120 ตัวอักษร
คำอธิบายรายการเมนู 300 ตัวอักษร
ที่อยู่ 500 ตัวอักษร
label หรือ value ของเวลาทำการ 80 ตัวอักษร
โทรศัพท์ 40 ตัวอักษร
อีเมล 254 ตัวอักษร
id ของ slide หรือ section 64 ตัวอักษร
id ของ media 64 ตัวอักษร
alt text ของ media 1000 ตัวอักษร
ชื่อ variant หรือ layout 64 ตัวอักษร
ฟิลด์ของฟอร์ม 12
ป้ายของฟิลด์ฟอร์ม 80 ตัวอักษร
คำอธิบายของฟิลด์ฟอร์ม 200 ตัวอักษร
ตัวเลือกในฟิลด์เลือก 20
ตัวเลือกหนึ่งรายการ 80 ตัวอักษร
ข้อความยินยอม 300 ตัวอักษร
ข้อความบนปุ่มฟอร์ม 40 ตัวอักษร
ข้อความหลังส่ง 300 ตัวอักษร
ค่าใน section ที่ลงทะเบียน 24
รายการในลิสต์ที่ลงทะเบียน 24
ข้อความของค่าที่ลงทะเบียน 2000 ตัวอักษร
ข้อความในช่องค้นหา 80 ตัวอักษร

ค่าคงที่คือ LIMITS และ SECTION_LIMITS

ข้อความเป็นข้อความธรรมดา heading, ย่อหน้า, label, ข้อความของรายการ และ run ที่จัดรูปแบบ ต้องไม่มี <, >, &lt;, &gt;, numeric entity, javascript: หรือ data:text/html ให้ render เป็นข้อความ และให้ระบบ template ของคุณ escape ตามปกติ

ลิงก์ (link.href และ href ใน run ที่จัดรูปแบบ) ต้องเป็นหนึ่งใน:

  • path บนไซต์เดียวกัน ขึ้นต้นด้วย / เพียงตัวเดียวพอดี
  • URL https:// ที่มี host แบบมีชื่อ (มีจุด)
  • ที่อยู่ mailto: ธรรมดาที่ไม่มี query

URL ของ media ถูกตรวจด้วย URL parser จริง:

URL ผลลัพธ์
https://cdn.example.com/a.jpg (อนุญาต query และ fragment) ยอมรับ
/local-media/abc (slash นำหน้าเพียงตัวเดียวพอดี) ยอมรับ
http://localhost…, http://127.0.0.1…, http://[::1]… ยอมรับ สำหรับการพัฒนาบน local
http:// บน host อื่นใด ปฏิเสธ
https:// ที่ไม่มี host ปฏิเสธ
https://user:pass@host/a.jpg ปฏิเสธ
//evil.example/a.jpg ปฏิเสธ
backslash หรือ whitespace ใด ๆ ปฏิเสธ
javascript:, data: หรือ scheme อื่นใด ปฏิเสธ

alt text ของ media ถูกตรวจความยาวแต่ไม่ถูกตรวจ markup ให้ใส่ไว้ใน attribute ที่ escape แล้ว

ใน rich_text และ text_media รายการใน paragraphs เป็น string หรือย่อหน้าที่จัดรูปแบบ:

type RichParagraph = {
inline: Array<{ text: string; bold?: true; italic?: true; href?: string }>;
list?: "bullet" | "number";
};

แต่ละความหมายมีวิธีเขียนแบบเดียว:

  • ใช้ object เฉพาะเมื่อมีตัวหนา ตัวเอียง ลิงก์ หรือลิสต์ ย่อหน้าธรรมดายังคงเป็น string object ที่ไม่มีการจัดรูปแบบจะถูกปฏิเสธด้วย noncanonical_default
  • bold และ italic เป็น true หรือไม่มี false จะถูกปฏิเสธด้วย noncanonical_default
  • run สองตัวที่อยู่ติดกันและมี mark และลิงก์เหมือนกันคือ run เดียว การเขียนแยกกันจะถูกปฏิเสธ ด้วย noncanonical_default
  • inline มี 1–32 run ข้อความของ run ไม่ถูก trim ดังนั้นช่องว่างระหว่าง run ยังอยู่ run ว่างไม่ได้ และย่อหน้าจะเป็นช่องว่างล้วนไม่ได้
  • รายการที่ต่อเนื่องกันและมี list เหมือนกันรวมเป็นลิสต์เดียว

paragraphBlocks(paragraphs) แปลงลิสต์ของย่อหน้าเป็น block สำหรับวาด: { kind: 'paragraph', runs } หรือ { kind: 'list', ordered, items } string ธรรมดากลายเป็น run เดียวที่ไม่มี mark ให้ escape run.text อย่าสร้าง HTML ดิบจากมัน paragraphText(paragraph) คืนคำของย่อหน้าหนึ่ง โดยไม่มีการจัดรูปแบบ

รายการวิดีโอระบุ provider และ id ของวิดีโอ มันไม่มีโค้ด embed หรือ URL ของ player เลย

provider video_id
youtube ตัวอักษรละติน ตัวเลข _ หรือ - จำนวน 11 ตัวพอดี
vimeo ตัวเลข 6 ถึง 12 หลัก

provider อื่นใด หรือ id ที่รูปแบบผิด จะถูกปฏิเสธด้วย unsafe_link กฎเหล่านี้คือกฎในตัวของ registry การฝัง (embed.ts) ชุดเดียวกับที่การฝังใน rich text ใช้ และตัวแก้ไข section อ่านลิงก์ดู แชร์ Shorts หรือ player ที่วางลงไปด้วย parser เดียวกัน

โดยค่าเริ่มต้น ให้วาด poster (หรือกรอบกลาง ๆ) และลิงก์ไปยังหน้าของ provider videoPageHref(item) คืนลิงก์นั้น อย่าส่ง request ไปยังบุคคลที่สามก่อนที่ผู้เข้าชมจะคลิก และ อย่าโหลด script ฝั่ง client theme ที่เล่นวิดีโอในหน้าจะสร้าง frame ของ player หลังการคลิก เท่านั้น จาก youtube-nocookie.com หรือ player.vimeo.com พร้อม dnt=1

DOCUMENT_REFUSALS แสดงทุกรหัสที่ parser คืนได้สำหรับเอกสารหน้า:

รหัส ความหมาย
unreadable_page ไม่ใช่เอกสารหน้า, ว่าง หรือ slide ที่ไม่มี id ที่ใช้ได้
unknown_slide_type ประเภท section ที่อยู่นอกรายการข้างต้น
invalid_slide โครงสร้างผิด ค่าที่จำเป็นหายไป มี property ที่ไม่รองรับ หรือหมายเลขโทรศัพท์หรืออีเมลมีรูปแบบไม่ถูกต้อง
markup_not_allowed ข้อความที่มี markup
unsafe_link ลิงก์ที่ผิดกฎของลิงก์ หรือ provider หรือ id ของวิดีโอที่ไม่อนุญาต
unknown_media media ที่ไม่อยู่ในคลังของไซต์ public parser ไม่เคยคืนรหัสนี้
duplicate_slide_target slide หรือ section สองตัวใช้ id เดียวกัน
unknown_section_variant variant ที่ type นั้นไม่มีให้
unknown_section_layout layout ที่ type นั้นไม่มี
missing_section_props section ที่ไม่มี props สำหรับ locale นี้
orphan_section_props props สำหรับ section ที่ไม่มีอยู่
too_many เกินขีดจำกัด detail.max ระบุขีดจำกัดนั้น
noncanonical_default ค่าที่เขียนในรูปแบบที่ข้อกำหนดไม่ใช้ ซึ่งรวมถึงฟิลด์ที่เพิ่มภายหลังที่ว่าง, false สำหรับ mark หรือ highlighted หรือย่อหน้าที่จัดรูปแบบแต่ไม่มีการจัดรูปแบบ และยังรวมถึง run สองตัวที่ควรเป็นตัวเดียว หรือ appearance ที่ว่าง
unknown_section_appearance คีย์หรือค่าของ appearance ที่ข้อกำหนดไม่มีให้
unknown_section_definition section ที่ลงทะเบียนปักเวอร์ชันที่ตัวอ่านข้อมูลนี้ไม่มี
unknown_component_version section ใน format 3 ปักเวอร์ชันคอมโพเนนต์ใช้ซ้ำที่ตัวอ่านข้อมูลนี้ไม่มี หรือไม่ตรงกับนิยามของมัน
shared_section_unexpanded renderer ได้รับ shared section มีเพียง parsePublicRenderablePage ที่คืนค่านี้

ข้อกำหนดยัง export:

Function หรือค่าคงที่ คืนค่า
paragraphBlocks(paragraphs) ย่อหน้าในรูป block สำหรับวาด ดู การจัดรูปแบบภายในข้อความ
paragraphText(paragraph) คำของย่อหน้าหนึ่ง โดยไม่มีการจัดรูปแบบ
videoPageHref(item) หน้าของ provider สำหรับรายการวิดีโอ
slideStrings(slide) ทุก string ที่ผู้เขียนเขียนและ slide แสดง ตามลำดับการอ่าน สำหรับ section ให้ส่ง slideFromSection(section)
pageMediaReferences(document) ทุกการอ้างอิง media ในเอกสาร รวมรายการในลิสต์
mapPageMedia(document, visit) เอกสารเดิมที่ทุกการอ้างอิง media ผ่าน visit มันไม่ย้าย section และไม่เปลี่ยน variant, layout หรือ appearance ของมัน
slideMediaPaths(type) ตำแหน่งที่ type หนึ่งเก็บ media ได้ ในรูป path ของคีย์ โดย * คือ index ในลิสต์
SLIDE_ITEM_MEDIA_KEYS คีย์ของ media ภายในรายการในลิสต์ แยกตาม type
currentPageContractVersion() เวอร์ชันข้อกำหนดที่สำเนานี้อ่านได้ (PAGE_CONTRACT_VERSION)
isPageContractVersion(value) ค่านั้นเป็นเวอร์ชันนี้หรือไม่ มีเวอร์ชันเดียว ค่าอื่นทั้งหมดได้ false
pageContractCapabilities(version) รายการหนึ่งของ PAGE_CONTRACT_VERSIONS
PAGE_CONTRACT_VERSIONS เวอร์ชันปัจจุบันของข้อกำหนดและสิ่งที่มันอ่านได้
sectionTypeName(type) ชื่อของ type ตามที่แอดมินแสดง เช่น Text with image
componentSectionTypes(version) type ที่ template วาดด้วย component ของตัวเองแยกตาม type: ทุก type ยกเว้น custom และ PLATFORM_DRAWN_TYPES
PLATFORM_DRAWN_TYPES type ที่ managed release วาดด้วย component ของแพลตฟอร์ม: search
DRAWN_SLIDE_TYPES, DrawnSlideType ทุก type ที่ renderer อาจได้รับ: ทั้งหมดยกเว้น shared
expandSharedSections(document, lookup) เอกสารที่แทนที่ shared section แต่ละตัวด้วย section ของ lookup(entry) เอกสารที่ได้จะผ่านการ parse เสมอ
sharedEntries(document), storedSharedEntries(raw) shared section ที่เอกสารที่ parse แล้วหรือที่เก็บไว้ใช้อยู่
isMediaValue(value) ค่าหนึ่งของ section ที่ลงทะเบียนเป็นการอ้างอิง media หรือไม่

function ที่รับ document จะรับเอกสารที่ parse แล้ว parsePublicPageDocument(raw) คืนเอกสารนั้นโดยไม่ต้องใช้ฐานข้อมูล

fixtures.ts ในไดเรกทอรีเดียวกัน เก็บเอกสารที่ Dee Wan ใช้ทดสอบ parser ของตัวเอง: ACCEPTED_PAGES, REFUSED_PAGES, STORED_ACCEPTED_PAGES และ STORED_REFUSED_PAGES ชุดเหล่านี้ครอบคลุม ทุกประเภท section ในตัว, ทุกฟิลด์ที่เพิ่มภายหลัง, ย่อหน้าที่จัดรูปแบบ, ทุกค่าของ appearance และค่าที่อยู่ ที่ขีดจำกัดแต่ละตัวพอดี V4_REFUSED_FORMS เก็บฟอร์มที่ถูกปฏิเสธ V4_CUSTOM_PAGES, V4_CUSTOM_REFUSED และ V4_CUSTOM_REFUSED_WITH_DEFINITION ครอบคลุม section ที่ลงทะเบียนเทียบกับ SECTION_DEFINITION_FIXTURE V5_SHARED_PAGE และ V5_SHARED_REFUSED ครอบคลุม shared section ส่วน V5_RENDERER_REFUSED คือสิ่งที่ renderer ต้องปฏิเสธ รัน renderer ของคุณกับชุดที่ยอมรับเพื่อตรวจว่ามันจัดการทุกประเภท section ได้ ชุดข้อมูลทดสอบเหล่านี้ไม่ถูก export จาก index.ts จึงไม่รวมอยู่ใน production bundle ของคุณ ให้ import ./fixtures เฉพาะในเทสต์

มีเลขสองตัว

  • format (format ในเอกสาร เป็น 2 หรือ 3) บอกว่าหน้าถูกเก็บอย่างไร format 3 มีการปักเวอร์ชัน ของคอมโพเนนต์ที่ใช้ซ้ำ สำเนาข้อกำหนดของคุณจะปฏิเสธ format ที่ไม่อยู่ใน PAGE_FORMATS แทนที่จะเดา
  • เวอร์ชันของข้อกำหนด (PAGE_CONTRACT_VERSION ตอนนี้คือ 5) ระบุคำศัพท์: ประเภท section, variant, layout, ฟิลด์, คีย์ของโครงสร้าง และขีดจำกัด การเปลี่ยนแปลงแบบเพิ่ม (type ใหม่, ฟิลด์ที่ไม่บังคับ, variant, ค่า layout หรือคีย์ของโครงสร้าง) จะเพิ่มเลขนี้ หน้าที่เก็บไว้ยังคง format เดิมและยังถูกต้องทุกไบต์

PAGE_CONTRACT_VERSIONS เก็บเฉพาะเวอร์ชันปัจจุบันและชุดที่แน่นอนของมัน มันอ่านได้เฉพาะชุดนั้น ไม่มากกว่า Dee Wan คำนวณ digest ของเวอร์ชันจากซอร์สของข้อกำหนดของตัวเอง เวอร์ชันที่ใหม่กว่าจะเป็น superset ของเวอร์ชันนี้

ทุก response ของ /v1 มี contracts.page.served ซึ่งเป็นเวอร์ชันข้อกำหนดที่สร้าง response นั้น ทุก item ที่เป็นหน้ามี page_contract.required ซึ่งเป็นเวอร์ชันข้อกำหนดที่อ่านหน้าของมันได้ เอกสารที่เก็บไว้ ไม่เปลี่ยน: fields.slides.format ยังคงเป็น format ของการเก็บ

{
"contracts": { "page": { "served": 5 } },
"data": {
"id": "ci_…",
"slug": "about",
"model": "page",
"locale": "en",
"version": 4,
"published_at": "2026-09-01T08:00:00.000Z",
"fields": {
"title": "About",
"slides": {
"format": 2,
"sections": [
{
"id": "intro",
"type": "hero",
"variant": "centred",
"layout": "default",
"component": null
}
],
"props": {
"intro": {
"heading": "About us",
"subheading": null,
"media": null,
"link": null
}
}
}
},
"relations": {},
"media": {},
"page_contract": { "required": 5 }
}
}
  • item ของ model ที่ไม่มีฟิลด์ slides ไม่มี page_contract เลย
  • เมื่อใช้ fields= จะคง page_contract ไว้เฉพาะเมื่อเลือก slides
  • item ที่อ่านหน้าไม่ได้จะไม่มี page_contract parser จะปฏิเสธหน้านั้นอยู่แล้ว

renderer ที่สร้างสำหรับเวอร์ชัน N จะ render หน้าเฉพาะเมื่อ required เป็น N หรือต่ำกว่า มิฉะนั้น มันจะถือว่าทั้งหน้าเป็นข้อผิดพลาดสำหรับ URL นั้นและแสดงหน้าข้อผิดพลาดทั่วไปของมัน มันไม่ render หน้าเพียงบางส่วน

Dee Wan ตรวจสอบ preview renderer ผ่าน /.well-known/dee-wan-renderer.json

  • version คือ 2 โปรโตคอลอื่นจะถูกปฏิเสธ (preview_contract_incompatible)
  • page_contract_version คือเวอร์ชันข้อกำหนดที่ใช้สร้าง renderer
  • renderer_contract_digest ต้องเท่ากับ digest ของเวอร์ชันนั้น มิฉะนั้น Dee Wan จะปฏิเสธ renderer ด้วย renderer_contract_unknown
  • page_formats ต้องเท่ากับ PAGE_FORMATS
{
"version": 2,
"site_id": "site_…",
"routes": { "page": "/preview/page/{slug}" },
"token_param": "preview",
"page_contract_version": 5,
"renderer_contract_digest": "<64 hex characters>",
"page_formats": [2, 3]
}

ไซต์ที่สร้างขึ้นให้บริการไฟล์นี้จาก src/lib/renderer-contract.ts การตรวจสอบที่เก็บไว้โดยไม่มี page_contract_version ถือว่ายังไม่ได้ตรวจสอบ: ให้ตรวจสอบ renderer ใหม่

Dee Wan ปฏิเสธการเผยแพร่หน้าที่ renderer ที่ใช้งานอยู่วาดไม่ได้ (renderer_capability_exceeded): section ที่ลงทะเบียนไว้ซึ่ง managed template ที่ใช้งานอยู่ไม่มี ไซต์ที่ไม่มี renderer ที่ตรวจสอบแล้วยังเผยแพร่ได้

editor_bridge ในข้อกำหนด renderer เป็นตัวเลือก Dee Wan อ่าน { "version": 1 } หรือ { "version": 3 } และทิ้งค่าอื่นทั้งหมด เวอร์ชัน 1 เลือก section ได้ เวอร์ชัน 3 ยังลิงก์ค่าที่แสดงผลแต่ละค่ากับฟิลด์ที่เก็บค่านั้น ไซต์ที่สร้างขึ้นประกาศเวอร์ชัน 3 ไซต์ที่สร้างไว้ก่อนหน้ายังคงเวอร์ชัน 1 และพรีวิวทำงานเหมือนเดิม editor จะแจ้งว่าลิงก์ฟิลด์ใช้ไม่ได้

เส้นทางลิงก์ฟิลด์ระบุค่าหนึ่งค่าของรายการหนึ่ง (field-links.ts) แต่ละส่วนเชื่อมด้วย / แต่ละส่วนคือคีย์ฟิลด์ ดัชนีในรายการ หรือ ~ ตามด้วย id ของอินสแตนซ์ที่เข้ารหัสแบบ percent-encoding: section ของหน้า อินสแตนซ์ ของ component (variant_id) หรือบล็อกแบบผสม (_id)

เส้นทาง ค่า
slides/~hero-1/heading หัวเรื่องของ section hero-1
slides/~hero-1/features/2/title ชื่อ feature ที่สามของ section นั้น
slides/~hero-1/link/label, slides/~hero-1/link ข้อความปุ่ม ลิงก์ของปุ่ม
slides/~hero-1/media ภาพของ section
faqs/~v1/question คำถามของอินสแตนซ์ component v1
body/3 บล็อกที่ 3 ของฟิลด์ rich
seo/image ภาพ SEO ในจุดที่มีการวาด

readFieldLinkPath ปฏิเสธการเขียนแบบอื่นทั้งหมด pageFieldLinks(sections) แสดงรายการค่าข้อความ ลิงก์ และสื่อที่หน้าอาจวาด แยกตาม section ข้อความทางเลือก ชื่อผู้ให้บริการ และ id ไม่อยู่ในรายการ entryFieldLinks(id, values, rich) แสดงรายการของหน้ารายการ เช่น โพสต์บล็อก: ค่าธรรมดาตามคีย์ฟิลด์ และค่า rich ที่เก็บไว้แต่ละค่าทีละบล็อก (body/3) พร้อมถ้อยคำของบล็อกและสื่อจากคลัง

ไซต์ที่สร้างขึ้นหา section จาก wrapper data-dw-section ส่วน managed renderer หา section จากคอมเมนต์ marker ของ section ตามตำแหน่ง ดังนั้น id ในรายการคือ id ของ marker นั้น หน้ารายการทำเครื่องหมายทั้ง article ด้วย data-dw-section และการเลือกนอกค่าที่อยู่ในรายการจะไม่ระบุสิ่งใด renderer ในตัวทั้งสองกำหนด route ให้โมเดล post ในข้อกำหนด renderer บนไซต์บล็อก

เฉพาะการแสดงผลพรีวิวเท่านั้นที่มีลิงก์ฟิลด์ route พรีวิวที่สร้างขึ้นเขียนรายการเป็น JSON (fieldLinksJson ซึ่งปลอดภัยภายในองค์ประกอบ <script>) และรัน bridge จากนั้น bridge หาองค์ประกอบที่แสดงแต่ละค่าและทำ เครื่องหมายด้วย data-dw-field (เส้นทาง) และ data-dw-occurrence (0 สำหรับองค์ประกอบแรกที่แสดงค่านั้น) และทำให้โฟกัสได้ ถ้าสองฟิลด์มีข้อความเดียวกัน ฟิลด์เหล่านั้นจะจับคู่กับองค์ประกอบตามลำดับ และเฉพาะเมื่อ จำนวนตรงกัน หน้าที่เผยแพร่ไม่มีรายการ เครื่องหมาย หรือ bridge เลย

ข้อความของเวอร์ชัน 3: frame ส่งข้อความไปยัง origin ของ editor ที่ตรงกับ editor_origin เท่านั้น และแนบ editor_nonce จากที่อยู่ของตน ทั้งสองฝั่งตรวจ window, origin, nonce และชุดคีย์ที่ตรงกันพอดี และไม่สนใจ ข้อความอื่น

จาก ข้อความ คีย์นอกเหนือจาก type, version: 3, nonce
frame dee-wan:preview:ready current_version_id, locale, model, entry, links
frame dee-wan:preview:pick path, occurrence
frame dee-wan:preview:edit path, occurrence, anchor
frame dee-wan:preview:close ไม่มี
editor dee-wan:preview:show path
editor dee-wan:preview:focus path
editor dee-wan:preview:clear ไม่มี

custom renderer ใช้เวอร์ชัน 3 กับ markup ของตนเองได้ โดยทำเครื่องหมายองค์ประกอบและส่งข้อความชุดเดียวกัน หากใช้ FIELD_LINK_BRIDGE ให้ใส่ marker ระบุตัวตนด้านล่าง พร้อม JSON ลิงก์ฟิลด์และองค์ประกอบหลักของ section ตามที่อธิบายไว้ข้างต้น แทนค่าตัวอย่างด้วยข้อมูลจริงของรายการที่แสดง data-dw-editor-version คือ id ของเวอร์ชันที่บันทึกอยู่ในปัจจุบัน ไม่ใช่เวอร์ชัน bridge 3 data-dw-editor-model คือ slug ของโมเดล data-dw-editor-entry คือ id ของรายการ และ data-dw-editor-locale คือภาษาที่แสดง ฝั่งตัวแก้ไขตรวจความพร้อมโดยเทียบเวอร์ชันและภาษา

<div
data-dw-editor-version="cver_example"
data-dw-editor-locale="en"
data-dw-editor-model="post"
data-dw-editor-entry="instance_example"
data-dw-editor-inplace
hidden
></div>

การมีแอตทริบิวต์ data-dw-editor-inplace จะเปิดการแก้ไขในพรีวิว หากต้องการปิด ให้ลบแอตทริบิวต์ออก การตั้งค่าเป็น "false" ยังถือว่าเปิดอยู่ หากไม่มีแอตทริบิวต์นี้ runtime จะส่ง pick เพื่อเปิดฟิลด์ใน ตัวแก้ไขตามปกติ หากไม่มี marker ระบุตัวตน runtime จะไม่เริ่มทำงาน แต่ยังใช้ตัวแก้ไขตามปกติได้

เมื่อเปิดโหมดนี้ การเลือกองค์ประกอบจะส่ง edit แทน pick bridge ที่เขียนเองส่งข้อความนี้ได้โดยตรง โดย target คือองค์ประกอบที่เลือก editorNonce มาจาก editor_nonce และ editorOrigin คือค่า editor_origin ที่ตรงกันพอดีจากที่อยู่ของ frame

const box = target.getBoundingClientRect();
window.parent.postMessage(
{
type: "dee-wan:preview:edit",
version: 3,
nonce: editorNonce,
path: "body/3",
occurrence: 0,
anchor: { x: box.x, y: box.y, width: box.width, height: box.height },
},
editorOrigin,
);

anchor คือสี่เหลี่ยมขอบเขตขององค์ประกอบที่เลือก หน่วยเป็น CSS พิกเซล อ้างอิงจากพื้นที่แสดงผลของ iframe ณ เวลาที่เลือก ใช้ getBoundingClientRect() โดยตรง ไม่ต้องบวกระยะเลื่อนหน้าเว็บหรือตำแหน่ง iframe บนหน้าหลัก ออบเจ็กต์ต้องมีเฉพาะ x, y, width, height แต่ละค่าเป็นตัวเลขจำกัดที่มีค่าสัมบูรณ์ไม่เกิน 100000 โดยความกว้างและความสูงต้องไม่ติดลบ ส่วน x/y ติดลบได้ occurrence เป็นจำนวนเต็มตั้งแต่ 0 ถึง 9999 ห้ามเพิ่ม anchor ใน pick ให้ส่ง edit แทน

path ต้องผ่าน readFieldLinkPath: ยาวไม่เกิน 512 อักขระ มีไม่เกิน 12 ส่วน และเริ่มด้วยคีย์ฟิลด์ คีย์ต้องตรงกับ [A-Za-z][A-Za-z0-9_]{0,63} ดัชนีเป็น 0–9999 โดยไม่มีเลขศูนย์นำหน้า ส่วนที่ระบุ อินสแตนซ์คือ ~ ตามด้วย id ที่ไม่ว่างและเข้ารหัสด้วย encodeURIComponent ในรูปแบบมาตรฐาน โดย id หลังถอดรหัสยาวไม่เกิน 128 หน่วยรหัส UTF-16 ข้อความและ anchor ต้องเป็นออบเจ็กต์ธรรมดาที่มีชุดคีย์ตรงตามที่ระบุ ข้อความที่มีคีย์เกินหรือค่าผิดขอบเขตจะถูกละเว้น links ในข้อความพร้อมใช้งานเป็นจำนวนเต็มตั้งแต่ 0 ถึง 999999

สำหรับรายการทั่วไป รองรับฟิลด์ text/rich ระดับบนสุดและบล็อกเดียวในฟิลด์ rich ที่ระบุด้วยดัชนี เช่น body/3 เส้นทางซ้อนใน component และหน้าที่ประกอบจาก section จะใช้การเลือกฟิลด์ตามปกติ ฝั่งตัวแก้ไขจะเปิดช่องเดิม เฉพาะฟิลด์ที่มองเห็นได้และผู้ใช้มีสิทธิ์แก้ไข หากเส้นทางไม่รองรับการแก้ไขในพรีวิว จะกลับไปเลือกฟิลด์ตามปกติ หากฟิลด์ไม่มีในแบบฟอร์ม ระบบจะแจ้งให้ทราบ แทนการแก้ข้อมูล frame ไม่เขียนข้อมูลลง CMS การแก้ไขจะเปลี่ยนค่าในแบบฟอร์มที่ยังไม่บันทึก โดยใช้สิทธิ์และ การตรวจค่าตามปกติ ปุ่ม เสร็จสิ้น ปิดการแก้ไขและคืนโฟกัสผ่าน dee-wan:preview:focus แต่ไม่บันทึกข้อมูล ปุ่ม บันทึก จึงจะบันทึกรายการ เมื่อได้รับ focus runtime จะเปิดส่วนที่ซ่อนอยู่และโฟกัสองค์ประกอบแรก ที่แสดงค่าของ path


ยังไม่เปิดตัว (pre-MVP) และยังไม่มีเวอร์ชันเสถียร
คอมมิตซอร์สโค้ด: e207e63239a627fbd2ae9c414ef7c6eb19fcfe5a (มีการแก้ไขที่ยังไม่คอมมิต)
วันที่สร้างเอกสาร (UTC): 2026-10-21T09:28:24.214Z
คอมมิตสำหรับอัปเกรด: ระบุไม่ได้: มีการแก้ไขที่ยังไม่คอมมิต. บันทึกการเปลี่ยนแปลง