การขอความยินยอมคุกกี้สำหรับ 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 ถูกออกแบบมาให้ทำงานร่วมกับเฟรมเวิร์กใดก็ได้ — หรือแม้แต่ไม่มีเฟรมเวิร์กเลย — โดยทำงานในระดับสคริปต์แทนระดับคอมโพเนนต์ เหตุผลที่เรื่องนี้สำคัญมีดังนี้:
- แท็กสคริปต์ซิงโครนัสเดียว: มีเพียงแท็ก
<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 ธรรมดา การเชื่อมต่อเหมือนกันทั้งหมด: ใช้เพียงแท็กสคริปต์เดียว วางให้ถูกที่