Skip to main content

ลิงก์รวมเนื้อหา

ภาพรวม

ลิงก์รวมเนื้อหา (Content Link) คือลิงก์ที่มี token ประจำตัว ซึ่งไม่ได้ชี้ไปยังบทความใดบทความหนึ่ง แต่เก็บ เงื่อนไขการคัดเลือกเนื้อหา เอาไว้ เมื่อผู้รับเปิดลิงก์ ระบบจะไปดึงเนื้อหาที่ตรงเงื่อนไข ณ ขณะนั้นมาแสดงเป็นรายการ

ประโยชน์คือทีมการตลาดสร้างลิงก์ครั้งเดียวแล้วนำไปติดในริชเมนู แคมเปญ หรือข้อความ LINE ได้เลย เนื้อหาที่ผู้ใช้เห็นจะอัปเดตตามบทความใหม่ที่ตรงเงื่อนไขโดยไม่ต้องแก้ลิงก์อีก

เงื่อนไขที่ตั้งได้ต่อหนึ่งลิงก์ประกอบด้วย หมวดหมู่, หมวดหมู่ย่อย, ช่วงวันที่เผยแพร่, กลุ่มผู้ชม (audience) ที่จำกัดสิทธิ์การดู และสถานะเปิด/ปิดใช้งาน แต่ละลิงก์มีตัวนับจำนวนคลิก และสามารถออก token ใหม่ได้ทันทีหากลิงก์รั่วไหล

หน้าจอทั้งหมดอยู่ในหน้าเดียว โดยฟอร์มสร้างและแก้ไขเปิดเป็นหน้าต่างซ้อน ไม่มีการเปลี่ยนหน้า

Business Flow

  1. เปิดหน้ารายการลิงก์รวมเนื้อหา ระบบโหลดรายการลิงก์พร้อมตัวเลือกหมวดหมู่สำหรับฟิลเตอร์ และดึงข้อมูล LINE OA ปัจจุบันเพื่อนำ LIFF ID มาใช้ประกอบลิงก์
  2. ตารางแสดงชื่อลิงก์, สรุปเงื่อนไขที่ตั้งไว้ในรูปแบบป้ายกำกับ (หมวดหมู่ / หมวดย่อย / ช่วงวันที่ / จำนวนกลุ่มผู้ชม หรือระบุว่ายังไม่มีเงื่อนไข), จำนวนคลิก, วันที่สร้าง และปุ่มคำสั่ง
  3. แถบเครื่องมือด้านบนมีตัวกรอง 3 ตัว ได้แก่ ค้นหาจากชื่อ, สถานะ และหมวดหมู่
  4. การสร้างลิงก์ใหม่ เปิดหน้าต่างฟอร์มซึ่งโหลดตัวเลือกหมวดหมู่และกลุ่มผู้ชมพร้อมกัน เมื่อผู้ใช้เลือกหมวดหมู่ ระบบจะล้างค่าหมวดย่อยเดิมแล้วโหลดตัวเลือกหมวดย่อยของหมวดนั้นให้ใหม่ (ช่องหมวดย่อยจะถูกปิดไว้จนกว่าจะเลือกหมวดหมู่)
  5. ฟิลด์ในฟอร์มประกอบด้วย ชื่อ (บังคับ), คำอธิบาย, กล่องเงื่อนไขการคัดเลือก (หมวดหมู่, หมวดย่อย, ช่วงวันที่, กลุ่มผู้ชมแบบเลือกได้หลายกลุ่ม) และสวิตช์เปิด/ปิดใช้งาน
  6. เมื่อบันทึก ระบบจะแปลงค่าฟอร์มเป็น payload โดยแปลงสวิตช์เป็นสถานะ active หรือ inactive, จัดรูปแบบช่วงวันที่เป็น YYYY-MM-DD และส่งค่าว่างเป็น null อย่างชัดเจน เพื่อให้การแก้ไขสามารถล้างเงื่อนไขเดิมออกได้จริง
  7. การคัดลอกลิงก์ ให้ผลต่างกันตามการตั้งค่า ถ้าลิงก์จำกัดกลุ่มผู้ชมและ OA มี LIFF ID ระบบจะสร้างลิงก์แบบ LIFF เพื่อให้เปิดในแอป LINE และระบุตัวตนผู้ใช้ได้ แต่ถ้าเป็นลิงก์สาธารณะจะได้ URL แบบเว็บปกติที่ประกอบจาก hash ของ OA และ token
  8. การออก token ใหม่ จะมีกล่องยืนยันเตือนว่าลิงก์เดิมจะใช้งานไม่ได้ทันที และการลบลิงก์ก็ต้องยืนยันเช่นกัน

หน้าจอและองค์ประกอบหลัก

หน้ารายการ (src/app/content-links/page.tsx) — รวมตาราง, ตัวกรอง, การคัดลอกลิงก์ทั้งแบบเว็บและแบบ LIFF, การออก token ใหม่ผ่าน POST /content-links/{id}/regenerate-token และการลบ

หน้าต่างฟอร์ม (src/app/content-links/components/ContentLinkFormModal.tsx) — ใช้ร่วมกันทั้งโหมดสร้างและแก้ไข รับผิดชอบการโหลดตัวเลือกทั้งหมด การผูกหมวดย่อยกับหมวดหมู่ที่เลือก และการประกอบ payload ก่อนส่ง

เซอร์วิสกลาง (src/services/content-link.service.ts) — ครอบคลุมการอ่านรายการ, อ่านรายตัว, สร้าง, แก้ไข, ลบ, ออก token ใหม่ และดึงเนื้อหาตัวอย่างของลิงก์ (preview) โดยฟังก์ชัน preview ไม่ได้ถูกเรียกจากหน้านี้ แต่ถูกใช้โดยตัวสร้างเมนูเพื่อ preview เมนูแบบแสดงเนื้อหาในตัว

โครงสร้างข้อมูลสำคัญ — ระเบียนลิงก์เก็บ token, จำนวนคลิก, ข้อมูล OA ที่สังกัด (ใช้ hash ประกอบ URL) และข้อมูลหมวดหมู่/หมวดย่อยที่อ้างอิง ส่วนผลลัพธ์ของ preview จะคืนรายการเนื้อหาพร้อม token สาธารณะ, slug, หัวเรื่อง, เกริ่นนำ, ภาพปก และวันที่เผยแพร่ ควบคู่กับข้อมูลการแบ่งหน้า

จุดเชื่อมต่อกับฟีเจอร์อื่น

  • สิทธิ์การเข้าถึง — ใช้ subject content-links เป็นคีย์ของเมนูย่อยใต้ Content Management ปลดล็อกจากโมดูล line-oa ฝั่งหลังบ้าน
  • จัดการเนื้อหา (Content Management) — เป็นแหล่งของเนื้อหาที่ถูกคัดมาแสดง
  • หมวดหมู่และหมวดหมู่ย่อย — เป็นเงื่อนไขคัดเลือกหลัก โดยตัวเลือกหมวดย่อยดึงจาก endpoint แบบ hierarchical dropdown
  • กลุ่มผู้ชม (Audience) — เมื่อกำหนดกลุ่มผู้ชม ลิงก์จะเปลี่ยนรูปแบบเป็น LIFF URL เพื่อให้ระบบระบุตัวตนผู้เปิดได้
  • ตัวสร้างเมนู (Menu Builder) — รายการเมนูชนิด content_link อ้างอิง token ของลิงก์ และใช้ endpoint preview เพื่อแสดงเนื้อหาในตัวอย่างเมนู
  • การตั้งค่าระบบ — URL สาธารณะประกอบจากตัวแปรสภาพแวดล้อม NEXT_PUBLIC_BASE_APP_URL และ API เรียกผ่าน NEXT_PUBLIC_BASE_CONSOLE_API_URL

รายละเอียดฝั่ง Backend (CMS API)

โมดูลนี้อยู่ที่ internal/modules/contentlink/ ลงทะเบียนไว้ใต้กลุ่ม /api/content-links ซึ่งอยู่ในโซนที่ต้องล็อกอิน ส่วนตัวลิงก์ที่ผู้รับเปิดจริงจะไปตกที่โมดูล public แยกกัน

สิทธิ์และการจำกัดขอบเขตข้อมูล

  • ทุก endpoint ของหน้านี้ต้องผ่าน JwtAuth ระดับ global
  • handler ประกาศ policy metadata ของโมดูล line-oa ไว้ (readAll / read / create / update / delete) แต่ ยังไม่บังคับใช้จริง และไม่มี ModuleGate คุม — การควบคุมที่ได้ผลจริงคือการซ่อนเมนูฝั่งหน้าจอ
  • service อ่าน userId, lineOaId, organizationId จาก context ของ request มาประกอบเป็นบริบทผู้ใช้ ทำให้ทั้งการลิสต์และการสร้างถูกผูกกับ OA ที่กำลังทำงานอยู่โดยอัตโนมัติ ไม่ได้รับค่าเหล่านี้จาก payload

token ของลิงก์

  • ตอน POST /api/content-links backend เป็นฝ่าย generate token เอง โดยสุ่มจากแหล่งสุ่มเชิงเข้ารหัส (crypto/rand) แล้วแปลงเป็น hex — หน้าจอไม่ได้กำหนด token และไม่ควรพยายามส่งเข้ามา
  • POST /api/content-links/:id/regenerate-token เขียน token ใหม่ทับของเดิม ลิงก์เดิมจึงใช้ไม่ได้ทันทีที่เรียกสำเร็จ ไม่มีช่วงผ่อนผันและไม่มีทางย้อนกลับไปใช้ token เก่า จึงเป็นเหตุผลที่หน้าจอต้องมีกล่องยืนยัน
  • เพราะ token คือสิ่งเดียวที่ป้องกันการเข้าถึงลิงก์สาธารณะ ใครที่ได้ token ไปก็เปิดดูรายการคอนเทนต์ได้ การจำกัดกลุ่มผู้ชม (audience) จึงเป็นชั้นที่ต้องพึ่งการระบุตัวตนผ่าน LIFF ไม่ใช่พึ่งความลับของ token

preview ก่อนส่งลิงก์จริง

GET /api/content-links/:id/preview-contents รับ page กับ limit และคืนรายการคอนเทนต์ที่ลิงก์นี้จะแสดงจริงโดยประเมินเงื่อนไข ณ เวลาที่เรียก ประโยชน์คือใช้ตรวจก่อนส่งว่าเงื่อนไขที่ตั้งไว้ให้ผลตามที่คิด (เช่น ช่วงวันที่แคบเกินไปจนไม่เหลือคอนเทนต์เลย) endpoint นี้ยังถูก Menu Builder เรียกใช้เพื่อ preview เมนูชนิดแสดงคอนเทนต์ในตัวด้วย

สิ่งที่เกิดขึ้นตอนผู้รับเปิดลิงก์

  • คำขอจากผู้รับไม่ได้เข้ามาที่ /api/content-links แต่ไปที่ endpoint สาธารณะ GET /api/public/contents ซึ่งไม่ต้องล็อกอิน
  • endpoint สาธารณะนั้นมี rate limit 10 ครั้งต่อ 60 วินาที ซึ่งเป็นเพดานที่ควรคำนึงถึงเมื่อส่งลิงก์เดียวให้คนจำนวนมากพร้อมกัน หรือเมื่อหน้าปลายทางเรียกซ้ำหลายรอบ (รายละเอียดอยู่ในเอกสารหน้าสาธารณะ)
  • ตัวนับจำนวนคลิกที่แสดงบนตารางถูกเพิ่มจากฝั่งการเปิดลิงก์สาธารณะ ไม่ได้เพิ่มจากการกด preview ในหลังบ้าน

ข้อสังเกตอื่น

  • DELETE /api/content-links/:id ตอบกลับด้วยสถานะ 204 (ไม่มีเนื้อหาใน body) ตามสัญญาเดิมของระบบ — โค้ดฝั่งเรียกจึงไม่ควรพยายามอ่าน JSON จาก response นี้
  • การแก้ไขผ่าน PATCH /api/content-links/:id รับ null ได้เพื่อล้างเงื่อนไขเดิมออก ตรงกับที่หน้าจอส่งค่าว่างเป็น null อย่างชัดเจน
  • ตารางที่เกี่ยวข้อง: content_link เป็นตารางหลัก อ่านประกอบกับ content_page, content_category, content_subcategory และ line_oa