การขอความยินยอมคุกกี้สำหรับ Next.js, Gatsby และ Static Site: คู่มือการเชื่อมต่อสำหรับนักพัฒนา
ปัญหาการขอความยินยอมบน Static Site
เฟรมเวิร์ก JavaScript สมัยใหม่อย่าง Next.js, Gatsby และ Nuxt.js ได้เปลี่ยนวิธีการสร้างและส่งมอบหน้าเว็บไปอย่างสิ้นเชิง หน้าเว็บจะถูก prerender ตอน build time หรือบนเซิร์ฟเวอร์ แล้วจึงถูกhydrate บนฝั่ง client สิ่งนี้สร้างความท้าทายเฉพาะสำหรับการขอความยินยอมค���กกี้: แบนเนอร์ขอความยินยอมต้องพร้อมก่อนที่สคริปต์ติดตามใด ๆ จะทำงาน แต่ตัวหน้าเพจอาจถูกเรนเดอร์และแคชไว้ที่ edge แล้ว
CMP แบบดั้งเดิมถูกออกแบบมาสำหรับหน้า PHP ที่เรนเดอร์บนเซิร์ฟเวอร์หรือหน้า HTML แบบง่าย ๆ ที่โหลดเอกสารจากบนลงล่างอย่างเป็นลำดับ ในโลกของเฟรมเวิร์กที่มี code splitting, lazy loading และการเรนเดอร์ฝั่งเซิร์ฟเวอร์แบบสตรีมมิง สมมติฐานเหล่านั้นใช้ไม่ได้อีกต่อไป การตั้งค่าการขอความยินยอมให้ถูกต้องในสภาพแวดล้อมเหล่านี้จำเป็นต้องเข้าใจกระบวนการเรนเดอร์ให้ดี
ทำไมเรื่องเวลา (Timing) ถึงสำคัญกว่าที่คิด
ในหน้า HTML ปกติ การวางสคริปต์ CMP ไว้ใน <head> ก่อนสคริปต์อื่น ๆ เป็นเรื่องตรงไปตรงมา แต่ใน Next.js App Router หรือ Gatsby สถานการณ์ซับซ้อนกว่านั้น:
- HTML ที่ prerender มาถึงก่อน: เบราว์เซอร์จะได้รับ HTML ที่สมบูรณ์จาก CDN หรือเซิร์ฟเวอร์ หากมี inline script หรือแท็ก third-party ฝังอยู่ใน HTML นั้น สคริปต์เหล่านั้นอาจทำงานก่อนที่ลอจิกการขอความยินยอมของคุณจะโหลดเสร็จ
- ช่องว่างระหว่างการ hydrate: การ hydrate ของ React จะเกิดขึ้นหลังจาก HTML ถูกวาดบนหน้าจอแล้ว หากคอมโพเนนต์ขอความยินยอมของคุณเป็น React component มันจะยังไม่อยู่ใน���ถานะที่ใช้งานได้จนกว่าการ hydrate จะเสร็จสมบูรณ์ ในช่วงช่องว่างนี้ Google tag หรือสคริปต์ analytics อื่น ๆ อาจยิงโดยไม่มีการขอความยินยอม
- ความซับซ้อนจาก edge caching: หากคุณใช้ ISR (Incremental Static Regeneration) หรือ edge function HTML จะถูกแคชไว้ คุณไม่สามารถ inject ลอจิกที่ขึ้นกับ consent เข้าไปใน HTML ที่แคชแล้วแบบไดนามิกได้ หากไม่มีเมคานิซึมฝั่ง client
หลักการสำคัญคือ: ต้องจัดการ consent ในระดับสคริปต์ ไม่ใช่ระดับคอมโพเนนต์ React component ที่เรนเดอร์แบนเนอร์ขอความยินยอมจะช้าเกินไป หากมันกลายเป็น interactive ได้ก็ต่อเมื่อการ hydrate เสร็จแล้วเท่านั้น
การเชื่อมต่อกับ Next.js App Router
Next.js 13+ ที่มาพร้อม App Router มีวิธีจัดการสคริปต์รูปแบบใหม่ วิธีที่แนะนำสำหรับการเชื่อมต่อระบบขอความยินยอมมีดังนี้:
ขั้นตอนที่ 1: โหลดสคริปต์ CMP ใน Root Layout
ใช้คอมโพเนนต์ Script ของ Next.js พร้อมกลยุทธ์ beforeInteractive ในไฟล์ layout.tsx ระดับ root วิธีนี้จะบอก Next.js ให้ inject สคริปต์เข้าไปในเอกสาร HTML แรกก่อนที่การ hydrate จะเริ่ม:
กลยุทธ์ beforeInteractive มีความสำคัญอย่างยิ่ง กลยุทธ์เริ่มต้น afterInteractive จะโหลดสคริปต์หลังจากการ hydrate ซึ่งช้าเกินไปสำหรับการขอความยินยอม ด้วย beforeInteractive สคริปต์ CMP จะถูกใส่ใน HTML ที่เรนเดอร์จากเซิร์ฟเวอร์และทำงานในขณะที่หน้าโหลด
ขั้นตอนที่ 2: ตั้งค่า Default Consent ก่อน Google Tag
ก่อนสคริปต์ Google Tag Manager หรือ gtag.js ให้ใส่ inline script ที่ตั้งค่าสถานะ consent เริ่มต้น วิธีนี้ทำให้มั่นใจได้ว่าแม้ GTM จะโหลดก่อนที่แบนเนอร์ CMP จะปรากฏ มันก็ยังเคารพค่าปฏิเสธเริ่มต้นเหล่านั้น:
inline script นี้ควรถูกวางไว้ใน <head> ของ root layout ก่อนสคริปต์ CMP และ GTM ใน Next.js คุณสามารถใช้แท็ก <script> ปกติภายใน element <head> ของ layout เพื่อจุดประสงค์นี้
ขั้นตอนที่ 3: จัดการการเปลี่ยนเส้นทาง (Route Changes)
ในการนำทางแบบ single-page application สคริปต์ CMP จะโหลดเพียงครั้งเดียว แต่การเปลี่ยน route จะไม่ทำให้หน้าโหลดใหม่ทั้งหน้า CMP ของคุณจึงต้องคงอยู่ตลอดการนำทางฝั่ง client FlexyConsent จัดการเรื่องนี้ให้อัตโนมัติ — เมื่อโหลดแล้ว มันจะยังคงทำงานอยู่ในทุกการเปลี่ยน route โดยไม่ต้อง initialize ใหม่
การเชื่อมต่อกับ Next.js Pages Router
สำหรับโปรเจกต์ที่ยังใช้ Pages Router วิธีการจะคล้ายกันแต่ใช้ _document.tsx แทน root layout ให้วางสคริปต์ CMP ไว้ในคอมโพเนนต์ <Head> ของ custom Document class กลยุทธ์ beforeInteractive ใช้งานได้แบบเดียวกันใน Pages Router
ความแตกต่างสำคัญคือ _document.tsx จะเรนเดอร์บนเซิร์ฟเวอร์เท่านั้น ดังนั้นลอจิกการขอความยินยอมใด ๆ ที่อยู่ในนี้จะถูกใส่ใน HTML แรกที่ถูกส่งออกไปอย่างแน่นอน
การเชื่อมต่อกับ Gatsby Static Site
Gatsby จะสร้าง HTML แบบ static เต็มรูปแบบตอน build time ไม่มีการเรนเดอร์ฝั่งเซิร์ฟเวอร์ในตอน request ซึ่งทำให้บางอย่างง่ายขึ้นแต่ก็ทำให้บางอย่างซับซ้อนขึ้น:
- ใช้
gatsby-ssr.tsxเพื่อ inject สคริปต์ CMP เข้าไปใน<head>ของทุกหน้า APIonRenderBodyช่วยให้คุณเพิ่มสคริปต์ลงใน head ที่จะอยู่ในไฟล์ HTML static ทุกไฟล์ - หลีก���ลี่ยงปลั๊กอิน Gatsby ที่ lazy-load การขอความยินยอม: ปลั๊กอินจากชุมชนบางตัวจะห่อ consent ไว้ใน React component ที่จะ mount หลังจากการ hydrate เท่านั้น ซึ่งจะสร้างช่องว่างด้านเวลาแบบที่กล่าวถึงไปแล้ว
- วางค่า default consent แบบ inline: ใช้
setHeadComponentsในgatsby-ssr.tsxเพื่อเพิ่ม inline script ที่ตั้งค่าสถานะ consent เริ่มต้น สคริปต์นี้จะอยู่ใน HTML static และทำงานทันที
แนวทาง build-time ของ Gatsby ทำให้ทุกไฟล์ HTML บน CDN ของคุณมีสคริปต์ consent อยู่ด้วย ซึ่งจริง ๆ แล้วถือว่าเหมาะมาก — ไม่มีลอจิกฝั่งเซิร์ฟเวอร์ให้ล้มเหลวหรือแคชผิดพลาด
ประเด็นที่ต้องคำนึงถึงใน Nuxt.js
Nuxt.js (พื้นฐานคือ Vue) มีรูปแบบของตัวเอง ใน Nuxt 3 ให้ใช้ composable useHead หรือการตั้งค่า app head ใน nuxt.config.ts เพื่อเพิ่มสคริปต์ CMP แบบ global Nuxt รองรับออปชัน body: false (ซึ่งจะวางสคริปต์ใน head) และแอตทริบิวต์ async สำหรับการโหลดแบบไม่บล็อกการเรนเดอร์
สำหรับโหมด server-side rendering ของ Nuxt หลักการเดียวกันยังใช้ได้: สคริปต์ CMP ต้องอยู่ใน HTML ตอบกลับแรก ไม่ใช่ถูก inject แบบไดนามิกโดย Vue component หลังจาก mount แล้ว
การหลีกเลี่ยง Layout Shift
แบนเนอร์ขอความยินยอมมักเป็นตัวการของ Cumulative Layout Shift (CLS) ซึ่งเป็น Core Web Vital ที่มีผลต่ออันดับ SEO เมื่อแบนเนอร์โผล��ขึ้นมาหลังจากหน้าเรนเดอร์แล้ว มันจะดันเนื้อหาลงหรือซ้อนทับเนื้อหาอย่างไม่คาดคิด
กลยุทธ์เพื่อลด CLS จากแบนเนอร์ขอความยินยอม:
- ใช้แบนเนอร์ที่อยู่ด้านล่าง: แบนเนอร์ที่อยู่ด้านล่างของ viewport จะไม่ดันเนื้อหาหน้าเว็บ วิธีนี้เป็นมิตรกับ CLS มากที่สุด
- จองพื้นที่ไว้ล่วงหน้า: หากจำเป็นต้องใช้แบนเนอร์ด้านบน ให้จองพื้นที่แนวตั้งไว้ใน CSS เพื่อให้ layout ของหน้าคำนึงถึงแบนเนอร์ตั้งแต่ก่อนเรนเดอร์
- หลีกเลี่ยง modal overlay ตอนโหลด: ผนังขอความยินยอมแบบเต็มหน้าจอที่โผล่ขึ้นมาหลังจ���กหน้าเรนเดอร์แล้วจะสร้างความรู้สึกว่า layout ไม่เสถียร หากจำเป็นต้องใช้ผนังแบบนี้ ให้เรนเดอร์มันเป็นส่วนหนึ่งของสถานะเริ่มต้นของหน้า
- โหลด CMP แบบ synchronous ใน head: เมื่อ CMP ถูกโหลดเป็นสคริปต์ที่บล็อกการเรนเดอร์ใน head แบนเนอร์จะสามารถปรากฏเป็นส่วนหนึ่งของการวาดครั้งแรก แทนที่จะโผล่มาทีหลัง
แนวทางที่ไม่ผูกกับเฟรมเวิร์กของ FlexyConsent
FlexyConsent ถูกออกแบบมาให้ทำงานร่วมกับเฟรมเวิร์กใดก็ได้ — หรือแม้แต่ไม่มีเฟรมเวิร์กเลย — โดยทำงานในระดับสคริปต์แทนระดับคอมโพเนนต์ เหตุผลที่เรื่องนี้สำคัญ��ีดังนี้:
- ใช้เพียงแท็กสคริปต์ async เดียว: มีเพียงแท็ก
<script>เดียวใน<head>ก็เพียงพอ ไม่ต้องติดตั้งแพ็กเกจ npm ไม่ต้องมี wrapper เฉพาะเฟรมเวิร์ก ไม่ต้องตั้งค่า build เพิ่มเติม - ค่า default consent ทำงานทันที: สคริปต์จะตั้งค่า Consent Mode V2 เริ่มต้นเป็นการกระทำแรก ก่อน callback หรือการจัดการ DOM ใด ๆ นั่นหมายความว่า Google tag จะเคารพ consent ตั้งแต่มิลลิวินาทีแรก
- ไม่พึ่งพา DOM: ลอจิกการขอความยินยอมจะไม่รอให้ React, Vue หรือ Svelte hydrate เสร็จ มันทำงานแยกจาก lifecycle ของเฟรมเวิร์ก
- ทำงานร่วมกับ SSG, SSR, ISR และ CSR: เพราะมันเป็น���พียงสคริปต์ธรรมดา มันจึงทำงานเหมือนกันไม่ว่าหน้าจะถูกสร้างแบบ static, เรนเดอร์บนเซิร์ฟเวิร์, regenerate แบบ incremental หรือเรนเดอร์บน client
เคล็ดลับสำหรับนักพัฒนา: วิธีทดสอบที่ง่ายที่สุดว่าคุณเชื่อมต่อ CMP ได้ถูกต้องหรือไม่ คือเปิดแท็บ Network ในเบราว์เซอร์ กรองด้วยโดเมนของ Google แล้วรีโหลดหน้า ห้ามมี request ไปยัง Google ใด ๆ เกิดขึ้นก่อนที่คำสั่งตั้งค่า consent เริ่มต้นจะปรากฏใน console หากมี แสดงว่า CMP ของคุณโหลดช้าเกินไป
แพ็กเกจฟรีของ FlexyConsent รองรับจำนวน pageview ไม่จำกัด และทำงานร่วมกับ Next.js, Gatsby, Nuxt, Astro, SvelteKit, Remix และ HTML ธรรมด��� การเชื่อมต่อเหมือนกันทั้งหมด: ใช้เพียงแท็กสคริปต์เดียว วางให้ถูกที่