ข้อกำหนดข้อมูลหน้า (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 ซึ่งเป็นเวอร์ชันข้อกำหนดต่ำสุดที่อ่านหน้านี้ได้
ดู ความเข้ากันได้
parser อยู่ที่ไหน
หัวข้อที่มีชื่อว่า “parser อยู่ที่ไหน”ข้อกำหนดนี้เป็น TypeScript module ขนาดเล็กที่ไม่มี dependency มันไม่ import อะไรนอกไดเรกทอรี ของตัวเอง จึงรันได้ใน Cloudflare Workers, ในเบราว์เซอร์ และใน Node
- ไซต์ที่สร้างขึ้น มีมันอยู่แล้ว คัดลอกมาโดยไม่เปลี่ยนแปลง ที่
src/lib/page-contract/ - กรณีอื่น มันอยู่ใน repository ของ Dee Wan ที่
backend/src/lib/page-contract/ไม่มี npm package ที่เผยแพร่ ให้คัดลอกทั้งไดเรกทอรีจาก Dee Wan เวอร์ชันที่คุณรัน และแทนที่มัน เมื่ออัปเกรด อย่าพิมพ์ type ใหม่ด้วยมือ
ใช้ parser ตัวไหน
หัวข้อที่มีชื่อว่า “ใช้ parser ตัวไหน”| 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 นั้น
format ที่รองรับ
หัวข้อที่มีชื่อว่า “format ที่รองรับ”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หรือ ใน sectioncustomเป็นการปักคอมโพเนนต์ - props ต้องไม่ซ้ำ
id,typeหรือคีย์ของ layout เช่นmedia_sideสิ่งเหล่านั้นเป็นของ โครงสร้าง - section อาจมี
appearanceในโครงสร้าง มันไม่บังคับ และเป็นของ locale นี้ เหมือนvariantดู รูปลักษณ์ของ section
สิ่งที่ parser ให้คุณ
หัวข้อที่มีชื่อว่า “สิ่งที่ parser ให้คุณ”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
ประเภทของ section
หัวข้อที่มีชื่อว่า “ประเภทของ section”ค่าที่ไม่บังคับจะคืนมาเป็น 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
รายการของ type แบบลิสต์
หัวข้อที่มีชื่อว่า “รายการของ type แบบลิสต์”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_defaultcontact: ต้องมีอย่างน้อยหนึ่งอย่างจากaddress,phone,email,map_linkหรือรายการhoursหนึ่งรายการ ถ้าไม่มีhoursจะอ่านเป็น[]phoneมีตัวเลข ช่องว่าง(,),.และ-โดยอาจมี+นำหน้า ยาวอย่างน้อย 3 ตัวอักษรemailคือที่อยู่เดียว ไม่ใช่ลิงก์mailto:map_link.hrefต้องขึ้นต้นด้วยhttps://logos: ทุกรายการต้องมีภาพ
latest_entries
หัวข้อที่มีชื่อว่า “latest_entries”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หรือfilenumberรับเลขฐานสิบที่มีค่าจำกัด รวมถึงรูปแบบเลขยกกำลัง และบันทึกเป็นตัวเลข 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 ที่ลงทะเบียน (custom)
หัวข้อที่มีชื่อว่า “Section ที่ลงทะเบียน (custom)”นักพัฒนาลงทะเบียน 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บังคับสำหรับ sectioncustomและถูกปฏิเสธใน type อื่น ในหน้า format 2componentยังเป็นnullformat นั้นไม่ตีความใหม่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>/ และระบบจะปฏิเสธบิลด์หากมีหน้าที่ตรึงเวอร์ชันซึ่งไม่สามารถรวมไว้ในบิลด์นั้นได้
คอมโพเนนต์ใช้ซ้ำ (format 3)
หัวข้อที่มีชื่อว่า “คอมโพเนนต์ใช้ซ้ำ (format 3)”คอมโพเนนต์ใช้ซ้ำคือ 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 และเฉพาะ sectioncustomที่มี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 นี้
Shared section (shared)
หัวข้อที่มีชื่อว่า “Shared section (shared)”ข้อกำหนดเวอร์ชัน 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) และห้ามมี sectionform,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 จะถูกบีบอัด
Variant และ layout
หัวข้อที่มีชื่อว่า “Variant และ layout”| 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
หัวข้อที่มีชื่อว่า “รูปลักษณ์ของ section”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 ที่จัดรูปแบบ
ต้องไม่มี <, >, <, >, 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)
คืนเอกสารนั้นโดยไม่ต้องใช้ฐานข้อมูล
การทดสอบ renderer ของคุณ
หัวข้อที่มีชื่อว่า “การทดสอบ renderer ของคุณ”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 และเวอร์ชันของข้อกำหนด”มีเลขสองตัว
- format (
formatในเอกสาร เป็น2หรือ3) บอกว่าหน้าถูกเก็บอย่างไร format3มีการปักเวอร์ชัน ของคอมโพเนนต์ที่ใช้ซ้ำ สำเนาข้อกำหนดของคุณจะปฏิเสธ format ที่ไม่อยู่ในPAGE_FORMATSแทนที่จะเดา - เวอร์ชันของข้อกำหนด (
PAGE_CONTRACT_VERSIONตอนนี้คือ5) ระบุคำศัพท์: ประเภท section, variant, layout, ฟิลด์, คีย์ของโครงสร้าง และขีดจำกัด การเปลี่ยนแปลงแบบเพิ่ม (type ใหม่, ฟิลด์ที่ไม่บังคับ, variant, ค่า layout หรือคีย์ของโครงสร้าง) จะเพิ่มเลขนี้ หน้าที่เก็บไว้ยังคง format เดิมและยังถูกต้องทุกไบต์
PAGE_CONTRACT_VERSIONS เก็บเฉพาะเวอร์ชันปัจจุบันและชุดที่แน่นอนของมัน มันอ่านได้เฉพาะชุดนั้น ไม่มากกว่า
Dee Wan คำนวณ digest ของเวอร์ชันจากซอร์สของข้อกำหนดของตัวเอง เวอร์ชันที่ใหม่กว่าจะเป็น superset
ของเวอร์ชันนี้
สิ่งที่ public API บอกคุณ
หัวข้อที่มีชื่อว่า “สิ่งที่ public API บอกคุณ”ทุก 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_contractparser จะปฏิเสธหน้านั้นอยู่แล้ว
renderer ที่สร้างสำหรับเวอร์ชัน N จะ render หน้าเฉพาะเมื่อ required เป็น N หรือต่ำกว่า มิฉะนั้น
มันจะถือว่าทั้งหน้าเป็นข้อผิดพลาดสำหรับ URL นั้นและแสดงหน้าข้อผิดพลาดทั่วไปของมัน มันไม่ render
หน้าเพียงบางส่วน
การประกาศเวอร์ชันของ renderer
หัวข้อที่มีชื่อว่า “การประกาศเวอร์ชันของ renderer”Dee Wan ตรวจสอบ preview renderer ผ่าน /.well-known/dee-wan-renderer.json
versionคือ2โปรโตคอลอื่นจะถูกปฏิเสธ (preview_contract_incompatible)page_contract_versionคือเวอร์ชันข้อกำหนดที่ใช้สร้าง rendererrenderer_contract_digestต้องเท่ากับ digest ของเวอร์ชันนั้น มิฉะนั้น Dee Wan จะปฏิเสธ renderer ด้วยrenderer_contract_unknownpage_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 และลิงก์ฟิลด์
หัวข้อที่มีชื่อว่า “Editor bridge และลิงก์ฟิลด์”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
คอมมิตสำหรับอัปเกรด: ระบุไม่ได้: มีการแก้ไขที่ยังไม่คอมมิต. บันทึกการเปลี่ยนแปลง