Content Management — Content Pages
ภาพรวม
Content Pages คือเมนูสร้างและแก้ไข "หน้าเนื้อหา" ของ LINE OA เช่นบทความ หน้าโปรโมชัน หรือหน้าข้อมูลบริการ ที่เปิดให้ผู้ใช้เข้าชมผ่านลิงก์หรือ QR code โดยไม่ต้องพัฒนาเว็บเพิ่ม เนื้อหาเก็บเป็น HTML แก้ผ่าน Rich Text Editor (Tiptap) และเก็บสองภาษา (ไทย/อังกฤษ) ไว้ในรายการเดียวกัน
จุดสำคัญที่ต้องรู้ก่อนเริ่มคือ หนึ่งหน้าเนื้อหามี slug ได้เพียงค่าเดียว แม้ฟอร์มจะมีช่อง "URL Slug" แยกไว้ให้ทั้งแท็บไทยและอังกฤษ ตอนบันทึกโค้ดหน้าเว็บจะเลือกค่าเดียวไปเก็บ (content.th.slug || content.en.slug) — slug ของภาษาอังกฤษจึงถูกใช้เฉพาะเมื่อฝั่งไทยไม่มี slug และหน้าที่แก้ไขอยู่จะดึง slug เดียวกันนั้นกลับมาแสดงในทั้งสองแท็บ
บทบาทที่ใช้งาน — ทีมการตลาดหรือทีมคอนเทนต์ที่ต้องเผยแพร่หน้าข้อมูลให้ลูกค้าเปิดดูได้เอง (โมดูลนี้ ไม่มี ModuleGate ที่ backend — ดูหัวข้อ สิทธิ์การเข้าถึง)
เส้นทางผู้ใช้งาน
1. ดูรายการหน้าเนื้อหา
เปิดเมนู Content Management > Content Pages ตารางแสดงคอลัมน์ ลำดับ, เนื้อหา, หมวดหมู่, การมองเห็น, สถานะ, จำนวนเข้าชม, อัปเดตล่าสุด และคอลัมน์การดำเนินการที่ตรึงไว้ทางขวา
- คอลัมน์ "เนื้อหา" แสดงชื่อเรื่องพร้อมป้ายภาษาที่มีจริง (TH สีน้ำเงิน / EN สีเขียว) ป้าย FT สีมะนาวถ้าผูก Friends Track ไว้ และ slug แบบ monospace ใต้ชื่อ (ตัดที่ 30 ตัวอักษร)
- คอลัมน์ "หมวดหมู่" แสดงเป็นป้ายคู่
หมวดหมู่ › หมวดหมู่ย่อย - คอลัมน์ "การมองเห็น" แสดงป้าย สาธารณะ เมื่อไม่จำกัดกลุ่มเป้าหมาย หรือป้าย "N กลุ่มเป้าหมาย" สีม่วงพร้อม tooltip ระบุชื่อกลุ่มทั้งหมด
- ตัวกรองมี 2 ช่อง — ค้นหาเนื้อหา และ สถานะ (ทั้งหมด / เผยแพร่แล้ว / ฉบับร่าง / เก็บถาวร) ทั้งสองช่องรีเซ็ตกลับหน้า 1 ทุกครั้งที่เปลี่ยนค่า มุมขวาของแถบเครื่องมือแสดงจำนวนหน้าทั้งหมด
2. สร้างหน้าเนื้อหาใหม่
กดปุ่ม สร้างเนื้อหา เข้าสู่หน้าฟอร์มแบ่งสองคอลัมน์ — ซ้ายคือแท็บแก้ไข 4 แท็บ ขวาคือตัวอย่างสดที่สลับกรอบจำลองได้ 3 ขนาด (เดสก์ท็อป = เต็มความกว้าง, แท็บเล็ต = 768px, มือถือ = 375px) พร้อมการ์ด ตัวอย่างผลการค้นหา ด้านล่างที่จำลองหน้าตาผลลัพธ์ Google
- แท็บ เนื้อหา — เลือก หมวดหมู่ (ไม่บังคับ) จากเมนู Categories ก่อน ช่อง หมวดหมู่ย่อย จะถูกปิดไว้จนกว่าจะเลือกหมวดหมู่ (placeholder เปลี่ยนเป็น "กรุณาเลือกหมวดหมู่ก่อน") เมื่อเลือกแล้วระบบเรียก dropdown แบบลำดับชั้นจากเมนู Subcategories มาเติมพร้อมคำใบ้ "หมวดหมู่ย่อยรองรับลำดับชั้นได้สูงสุด 4 ระดับ" — และการเปลี่ยนหมวดหมู่จะล้างหมวดหมู่ย่อยที่เลือกไว้ทิ้งทุกครั้ง
- ถัดลงมาเป็นแท็บภาษา ภาษาไทย (TH) / English (EN) แต่ละภาษามีช่อง หัวข้อ (Title), URL Slug และ เนื้อหา (Content) เมื่อกรอกหัวข้อ ระบบสร้าง slug ให้อัตโนมัติ (ทับค่าที่แก้ไว้ทุกครั้งที่พิมพ์หัวข้อ) ปุ่ม แปลอัตโนมัติ มุมขวาของแท็บยังเป็น placeholder — กดแล้วขึ้น alert ว่า "ฟีเจอร์แปลภาษาจะเปิดให้ใช้งานเร็วๆ นี้!"
- แผงพับ SEO Settings ในแต่ละภาษามี Meta Title, Meta Description (นับตัวอักษร สูงสุด 160), Excerpt (สูงสุด 200), OG Title (สูงสุด 160), OG Description (สูงสุด 200) และ OG Image URL
- แท็บ การควบคุมการเข้าถึง — เลือก จำกัดกลุ่มเป้าหมาย ได้หลายกลุ่มจากเมนู Segment audience และ/หรือเปิดสวิตช์ ป้องกันด้วยรหัสผ่าน ทั้งสองช่องนี้ใช้ร่วมกันได้ แต่ช่องกลุ่มเป้าหมายจะถูก ปิด ทันทีที่ผูก Friends Track ไว้ และกลับกันด้วย
- แท็บ Friends Track — เลือกแคมเปญเพิ่มเพื่อนสถานะ active จากเมนู Friend track เมื่อเลือกแล้วระบบโชว์การ์ดสีเขียวระบุชื่อและ token ของแคมเปญ ช่อง ข้อความปุ่ม CTA (สูงสุด 50 ตัวอักษร ค่าเริ่มต้น "เพิ่มเพื่อน") และตัวอย่างปุ่มสีเขียว LINE (
#06C755) ที่จะติดอยู่ท้ายหน้าเนื้อหาจริง - แท็บ ตั้งค่าแคมเปญ — พารามิเตอร์ UTM (utmSource/utmMedium เป็น select แบบ tags มีตัวเลือกสำเร็จรูป, utmCampaign/utmTerm/utmContent เป็นช่องข้อความ) และการวิเคราะห์ (Google Analytics Events แบบ JSON, Facebook Pixel Events, LINE Tag Events, สคริปต์ Head ที่กำหนดเอง)
- กด บันทึกเป็นฉบับร่าง หรือ เผยแพร่ — ปุ่มทั้งสองถูก disable ไว้จนกว่าจะมีหัวข้อหรือเนื้อหาอย่างน้อยหนึ่งภาษา และก่อนยิง API ยังตรวจซ้ำอีกสองชั้นว่า "กรุณากรอกเนื้อหาอย่างน้อยหนึ่งภาษา" และ "กรุณาระบุ URL slug อย่างน้อยหนึ่งภาษา"
3. แก้ไขและสลับสถานะ
กดไอคอนดินสอในตารางเพื่อเข้าหน้าแก้ไข (/content-management/edit?id=…) หน้าตาเหมือนฟอร์มสร้างแต่มีป้ายสถานะกำกับหัวข้อ และแถบปุ่มเปลี่ยนตามสถานะปัจจุบัน:
- ฉบับร่าง → มีปุ่ม บันทึกการเปลี่ยนแปลง และ เผยแพร่
- เผยแพร่แล้ว → มีปุ่ม บันทึกการเปลี่ยนแปลง และ ยกเลิกการเผยแพร่ (ส่ง
status = draftกลับไป) - ช่องรหัสผ่านในหน้าแก้ไขเปลี่ยน placeholder เป็น "เว้นว่างเพื่อคงรหัสผ่านเดิม"
4. เผยแพร่ต่อไปยังช่องทางอื่น
จากหน้ารายการ กดไอคอนลิงก์เพื่อคัดลอกลิงก์ (ปุ่มถูก disable ถ้าหน้ายังไม่มี publicToken หรือระบบยังไม่รู้ค่า LINE OA hash) หรือไอคอน QR เพื่อเปิดกล่อง QR code ขนาด 256px ที่ ดาวน์โหลด PNG, คัดลอกลิงก์ หรือปิดได้ รูปแบบลิงก์ที่ได้ต่างกันตามการตั้งค่า:
- มีการจำกัดกลุ่มเป้าหมาย + OA มี LIFF ID →
https://liff.line.me/{liffId}/{lineOaHash}/content/…(ข้อความยืนยัน "คัดลอกลิงก์ LIFF แล้ว! (เปิดในแอป LINE)") - ไม่จำกัดกลุ่ม →
{BASE_APP_URL}/{lineOaHash}/content/…
นำลิงก์ไปฝังต่อในเมนู Menu Builder, ริชเมนูที่เมนู Rich menu หรือแคมเปญบรอดแคสต์ที่เมนู Campaign
รายละเอียดสำคัญที่ตรวจสอบจากซอร์สโค้ดจริง
ยืนยันจาก internal/modules/contentpage/{controller,service,dto,helpers}.go และ internal/modules/publicmod/controller.go ของ line-management-cms-api-go:
- slug ถูกบังคับรูปแบบเข้มงวดที่ backend แต่ฝั่งหน้าเว็บไม่ตรวจเลย — backend ใช้ regex
^[a-z0-9-]+$และตอบกลับข้อความ "Slug must contain only lowercase letters, numbers, and hyphens" (ยาวได้ไม่เกิน 255 ตัวอักษร) ส่วนฟอร์มปล่อยให้พิมพ์อะไรก็ได้ในช่อง URL Slug — ถ้าแก้ slug เองแล้วเผลอใส่ตัวพิมพ์ใหญ่ ขีดล่าง หรืออักษรไทย จะโดนปฏิเสธตอนกดบันทึก ไม่ใช่ตอนพิมพ์ ตัวสร้าง slug อัตโนมัติของหน้าเว็บมี ตารางถอดอักษรไทยเป็นละตินในตัว (ก→k, ข/ฃ/ค/ฅ/ฆ→kh, เ→e, ไ/ใ→ai ฯลฯ) แล้วจึงตัดอักขระที่เหลือทิ้ง — วรรณยุกต์และไม้ทัณฑฆาตไม่มีในตาราง จึงถูกตัดหายไปเงียบ ๆ - slug ต้องไม่ซ้ำ "ต่อหนึ่ง LINE OA" ไม่ใช่ทั้งระบบ — ตรวจด้วยเงื่อนไข
line_oa_id + slug + deleted_date IS NULLถ้าซ้ำจะได้ 409 "A content page with this slug already exists" ส่วนตอนแก้ไขจะตรวจเฉพาะเมื่อ slug เปลี่ยนจากค่าเดิม — บันทึกซ้ำโดยไม่แตะ slug จึงไม่ถูกปฏิเสธ - กลุ่มเป้าหมายกับ Friends Track ห้ามอยู่ด้วยกัน และ backend บังคับจริง — ทั้ง create และ update ตรวจว่าถ้ามี
audienceIdsอย่างน้อยหนึ่งค่าและมีfriendTrackCampaignIdพร้อมกัน จะถูกปฏิเสธด้วย 400 พร้อมเหตุผลเต็ม ๆ ว่า "Audience Restriction requires authentication which blocks the publicly accessible Friend Track CTA." ที่สำคัญคือตอน update ระบบไม่ได้ตรวจแค่ค่าที่ส่งมา แต่ รวมค่าเดิมในฐานข้อมูลกับค่าที่ส่งมาก่อน แล้วจึงตรวจ — ส่งaudienceIdsมาเพียงตัวเดียวเพื่อจะแก้เนื้อหาหน้าที่ผูก Friends Track อยู่แล้ว ก็ยังถูกปฏิเสธ - public token ออกครั้งเดียวและไม่เปลี่ยนอีกเลย — เมื่อบันทึกด้วยสถานะ
publishedครั้งแรก backend สุ่มcrypto.randomBytes(32)เป็น hex 64 ตัวอักษรเก็บไว้เป็นpublic_tokenพร้อมประทับpublishedAt/publishedByแต่ตอนแก้ไข เงื่อนไขออก token ใหม่คือ "ยังไม่มี token" เท่านั้น ดังนั้นวนยกเลิกเผยแพร่แล้วเผยแพร่ใหม่กี่ครั้ง ลิงก์เดิมยังเป็นลิงก์เดียวกันตลอด — และเมนูนี้ ไม่มีปุ่มออก token ใหม่ (ต่างจากเมนู Content Links และ Menu Builder ที่มี) - จำนวนเข้าชมนับทุกครั้งที่โหลด และคอลัมน์ "ไม่ซ้ำ" ไม่เคยขยับ —
GET /api/public/content/:tokenเพิ่มview_countทีละ 1 ทุกครั้งที่อ่านสำเร็จ (รวมทั้งกดรีเฟรชซ้ำและกด preview จากหน้ารายการ) ส่วนunique_view_countต้องเพิ่มจากฟังก์ชันtrackViewที่บันทึก session — แต่ handler ของ endpoint ติดตามผลนั้น แค่ตรวจว่าเนื้อหามีอยู่จริงแล้วตอบข้อความ "View tracked successfully" กลับไปโดยไม่เขียนอะไรลงฐานข้อมูลเลย (ความเห็นในโค้ดระบุว่ายกมาจากต้นฉบับที่เขียนว่า "we'll skip the actual tracking implementation") ทำให้สถิติ analytics ทั้งชุด (viewsByDate, topReferrers, deviceTypes) ไม่มีข้อมูลป้อนเข้า - มีสองเส้นทางสาธารณะสำหรับอ่านหน้าเดียวกัน และตอบต่างกันเมื่อรหัสผ่านผิด —
GET /api/public/content-pages/:tokenตอบ 403 "Password required" / "Invalid password" ส่วนGET /api/public/content/:token(เส้นทางที่หน้าเว็บฝั่งผู้ใช้เรียกจริง) ตอบ 200 พร้อม body{ requirePassword: true, message: "…" }เพื่อให้หน้าเว็บเรนเดอร์ฟอร์มกรอกรหัสผ่านได้ — เส้นทางหลังยังตรวจกลุ่มเป้าหมายด้วยx-liff-tokenheader (ไม่มี header → 403 "Authentication required for this content") ในขณะที่เส้นทางแรกไม่ตรวจกลุ่มเป้าหมายเลย - ปุ่มลบเป็น soft delete แบบเจาะจงคอลัมน์ —
DELETE /api/content-pages/:idตอบ 204 ว่างเปล่า และเขียนแค่ 3 คอลัมน์ (deleted_date,deleted_by,updated_date) ทิ้งupdated_byไว้เท่าเดิมโดยเจตนา ตามที่ความเห็นในโค้ดอธิบายว่าเป็นการทำซ้ำพฤติกรรม partial-select ของ TypeORM เดิม - การอัปโหลดรูปในตัวแก้ไขจำกัดชนิดไฟล์ที่ backend เท่านั้น — Rich Text Editor รับ
accept="image/*"และอัปโหลดได้ทั้งจากปุ่ม ลากวาง และวางจากคลิปบอร์ด แต่ backend อนุญาตแค่image/jpeg,image/jpg,image/png,image/gif(สูงสุด 10MB) — WebP, AVIF หรือ SVG จึงผ่านหน้าเว็บไปแล้วโดนปฏิเสธด้วย "Invalid file type. Only JPEG, PNG, and GIF are allowed" ไฟล์ที่อัปโหลดสำเร็จตั้งชื่อเป็นcontent-image-{timestamp}แยกโฟลเดอร์ตาม LINE OA hash
เมนูที่เกี่ยวข้องกันในเส้นทางนี้
- Categories — เป็นแหล่งของตัวเลือก "หมวดหมู่" ในแท็บเนื้อหา (เรียก endpoint dropdown เฉพาะ)
- Subcategories — โหลดต่อจากหมวดหมู่ที่เลือก โดยใช้ dropdown แบบลำดับชั้นที่เยื้องระดับให้เห็นโครงต้นไม้
- Content Links — เมนูพี่น้องที่กรองเนื้อหาจากหมวดหมู่/หมวดหมู่ย่อยชุดเดียวกันนี้ แล้วรวมหน้าเนื้อหาที่ตรงเงื่อนไขเป็นลิงก์เดียว
- Segment audience — เป็นแหล่งของกลุ่มเป้าหมายในแท็บการควบคุมการเข้าถึง และเป็นตัวชี้ว่าลิงก์ที่คัดลอกจะออกมาเป็นแบบ LIFF หรือเว็บปกติ
- Friend track — เป็นแหล่งของแคมเปญเพิ่มเพื่อนในแท็บ Friends Track (ดึงเฉพาะสถานะ active) และหน้าเนื้อหาจะกลายเป็นหน้า landing ของแคมเปญนั้น
- Menu Builder — รายการเมนูชนิด "หน้าเนื้อหา" อ้างอิงหน้าที่เผยแพร่แล้วจากเมนูนี้โดยตรง
- Rich menu / Campaign — ปลายทางที่นำลิงก์หรือ QR code ที่คัดลอกได้ไปฝัง
จุดที่ควรระวัง
- ลิงก์ที่คัดลอกได้ใช้ slug ไม่ใช่ token — โค้ดหน้ารายการสร้างเส้นทางจาก
record.slug || record.publicToken(slug มาก่อนเสมอ และ slug บังคับกรอกอยู่แล้ว) แต่หน้าอ่านสาธารณะและ APIGET /api/public/content/:tokenค้นหาด้วยpublic_tokenเท่านั้น ถ้าเปิดลิงก์ที่คัดลอกมาแล้วได้ "Content not found" ให้ตรวจค่าpublicTokenของหน้านั้นก่อน - ปุ่มคัดลอกลิงก์/QR ถูก disable เมื่อระบบยังไม่รู้ค่า LINE OA hash — ค่านี้มาจากการเรียก
GETข้อมูล LINE OA แยกอีกครั้งหลังหน้าโหลด ถ้าเรียกล้มเหลวจะขึ้นเพียง error ใน console แล้วปุ่มยังคง disable อยู่เงียบ ๆ - สถานะ เก็บถาวร มีในตัวกรองและ backend มี endpoint
POST /content-pages/:id/archiveรองรับ แต่ ไม่มีปุ่มใดในหน้าเว็บที่ตั้งสถานะนี้ ตัวเลือกในตัวกรองจึงคืนผลว่างอยู่ตลอดในทางปฏิบัติ - ตัวอย่าง "URL ที่สร้างขึ้น" ในแท็บตั้งค่าแคมเปญแสดงรูปแบบ
{origin}/th/{slug}และ{origin}/en/{slug}ซึ่ง ไม่ใช่รูปแบบลิงก์จริง ที่ระบบคัดลอกให้ ({base}/{lineOaHash}/content/…) ใช้เป็นภาพประกอบเรื่อง UTM เท่านั้น - ช่อง slug ถูกทับด้วยค่าที่สร้างอัตโนมัติทุกครั้งที่แก้หัวข้อ — ถ้าตั้งใจใช้ slug ที่กำหนดเอง ให้แก้หัวข้อให้เสร็จก่อนแล้วจึงมาแก้ slug
- ผู้ใช้คนอื่นในองค์กรเดียวกันแก้/ลบหน้าเนื้อหาของเราได้ — การตรวจสิทธิ์เจ้าของที่ backend ยอมรับเมื่อ ผู้ใช้เดียวกัน หรือ องค์กรเดียวกัน ก็พอ ตราบใดที่อยู่ LINE OA เดียวกัน
สิทธิ์การเข้าถึง (ตรวจสอบจากซอร์สโค้ดจริง)
- ฝั่งหน้าเว็บ — หน้ารายการและฟอร์มของเมนูนี้ ไม่ได้ครอบด้วย
PermissionGuard(ต่างจากหน้าอย่าง Campaign ที่ครอบไว้จริง) สิ่งเดียวที่ตรวจสิทธิ์คือ การแสดงเมนูในแถบด้านข้าง ซึ่งเช็คability.can(VIEW, 'content-management')ก่อนจะแสดงกลุ่มเมนู Content Management กับลูกทั้ง 4 รายการ — ถ้าผู้ใช้ไม่มีสิทธิ์นี้ เมนูจะหาย แต่พิมพ์ URL/content-managementตรง ๆ ยังเข้าหน้าได้ - ฝั่งเซิร์ฟเวอร์ — ทุกเส้นทางประกาศ policy ระดับการกระทำไว้ (
READALL/READ/CREATE/UPDATE/DELETEบน subjectline-oa) แต่auth.CheckPoliciesเป็น middleware ที่ปล่อยผ่านทุกคำขอ ตามที่ความเห็นในโค้ดระบุตรง ๆ ว่า "Enforcement disabled — allow all authenticated requests (TS parity)" การบังคับสิทธิ์จริงจึงเหลือแค่ JWT + การตรวจสิทธิ์เจ้าของ (validateOwnership: Super Admin ผ่านหมด, คนอื่นต้องอยู่ LINE OA เดียวกัน และต้องเป็นเจ้าของหรืออยู่องค์กรเดียวกัน) - ไม่มี
ModuleGateในโมดูลนี้ — ตรวจการลงทะเบียนเส้นทางในcontentpage/controller.goแล้วไม่พบการเรียกmodulegate.ModuleGate(d, "...")เลย (ต่างจากโมดูลcampaignที่มี) และชุดโมดูลที่มี gate จริงตามรายการในmodulegate/coverage_test.goก็ไม่มี content-page อยู่ด้วย ที่น่าสังเกตคือโมดูลนี้ ไม่ได้อยู่ในรายการexemptModulesที่บันทึกโมดูลซึ่ง "ตั้งใจไม่ให้ gate" ไว้เช่นกัน — ตามคำอธิบายในเทสต์นั้น โมดูลที่ไม่มี gate หมายความว่าผู้ดูแลระดับแพลตฟอร์ม ปิดโมดูลนี้ให้องค์กรใดไม่ได้เลย เพราะไม่มีจุดที่บังคับ - เส้นทางสาธารณะ 2 เส้นทาง (
/api/public/content-pages/:tokenและ/api/public/content/:token) ไม่ต้องมี JWT โดยเจตนา — การป้องกันมาจาก token ที่เดายาก + รหัสผ่าน (bcrypt cost 10) + การตรวจกลุ่มเป้าหมายผ่านx-liff-token - ขอบเขตข้อมูลถูกจำกัดด้วย LINE OA เสมอ —
GET /api/content-pagesกรองด้วยline_oa_idของผู้เรียกเป็นเงื่อนไขแรกทุกครั้ง
ไฟล์อ้างอิงจากซอร์สโค้ด
Frontend (cms-web-2026)
src/app/content-management/page.tsx— หน้ารายการ, ตัวกรอง, ปุ่มคัดลอกลิงก์/QR, การประกอบ URL แบบ LIFF vs เว็บปกติsrc/app/content-management/create-multilang/page.tsx,src/app/content-management/edit/page.tsx— ฟอร์ม 4 แท็บ, ตัวอย่างสด, ตรรกะปุ่มบันทึก/เผยแพร่/ยกเลิกการเผยแพร่src/components/content-management/MultiLanguageForm.tsx— แท็บภาษา, การสร้าง slug อัตโนมัติ, แผงพับ SEOsrc/components/content-management/TiptapEditorSimple.tsx— Rich Text Editor และการอัปโหลดรูป (ปุ่ม/ลากวาง/วาง)src/utils/slugify.ts— ตารางถอดอักษรไทยเป็นละตินsrc/services/content-page.service.ts— endpoint ที่หน้าเว็บเรียกใช้src/components/layout/sidemenu/sidemenu.container.tsx— การตรวจสิทธิ์ระดับเมนูcontent-management
Backend (line-management-cms-api-go)
internal/modules/contentpage/controller.go— 12 เส้นทาง (รวม 2 เส้นทางสาธารณะ), การอัปโหลดรูป, การไม่มี ModuleGateinternal/modules/contentpage/service.go— ตรรกะสร้าง/แก้ไข/ลบ/ทำสำเนา, การออก public token,validateOwnership, การนับ viewinternal/modules/contentpage/dto.go— regex ของ slug และกฎตรวจสอบข้อมูลขาเข้าทั้งชุดinternal/modules/publicmod/controller.go—GET /public/content/:token,liff-config, การตรวจกลุ่มเป้าหมายและรหัสผ่านของฝั่งผู้ใช้internal/modules/auth/policy.go—CheckPoliciesที่เป็น no-opinternal/modules/modulegate/coverage_test.go— รายการโมดูลที่มี ModuleGate จริงและโมดูลที่ยกเว้น
ดูรายละเอียดเชิงเทคนิคเพิ่มเติม (เวอร์ชันก่อนหน้าของทีมเอกสาร) ที่ จัดการหน้าเนื้อหา (Content Page)