跳到主要內容
← 回到學習

學習 · 資安

Termly 第三方cookie教學

目錄 · 8 個段落
  1. 01為什麼要整合 Termly(或類似工具)
  2. 02Next.js <Script> 為何不行
  3. 03解法:useEffect 動態注入
  4. 04TypeScript:.d.ts 宣告 window.displayPreferenceModal
  5. 05auto-block 機制
  6. 06替代方案
  7. 07自己寫(若不想依賴 SaaS)
  8. 08常見地雷
TL;DR
Termly 是 cookie consent + 隱私政策產生器,GDPR/CCPA 合規工具。Next.js 的 <Script> 元件 不太能正確載 Termly 的 embed(常踩坑),改用 useEffect 動態 inject <script> + cleanup。TypeScript 要 .d.ts 宣告 window.displayPreferenceModal 才能呼叫 cookie 偏好彈窗。
主要參考
Stack Overflow — How to get script from Termly to run in Next.js

為什麼要整合 Termly(或類似工具)

合規需求:

  • GDPR(歐盟):必須讓用戶選擇接受 / 拒絕 cookie
  • CCPA(加州):用戶可以「Do Not Sell」
  • 台灣個資法:類似要求(2025 起趨嚴)

DIY 寫一套 cookie consent banner 很煩:

  • 動態 block / unblock Google Analytics、廣告 script
  • 多語、UI 一致性
  • 法規變動時要追

Termly / Cookiebot / OneTrust 等 SaaS 解決方案 處理這些細節。


Next.js <Script> 為何不行

// ❌ 看似合理,實際上 Termly 不會正常載入
import Script from 'next/script';
 
<Script
  src="https://app.termly.io/embed.min.js"
  data-auto-block="on"
  data-website-uuid="xxx"
/>
tsx

問題:

  • Next.js <Script> 內部會處理 async / defer,但 Termly 的 embed 對 script 載入時機很敏感
  • data-* attribute 在 React 寫法中可能被吞掉(部分 Next.js 版本)
  • 載入順序對 cookie blocking 很重要,<Script strategy="afterInteractive"> 太晚

解法:useEffect 動態注入

import { useEffect } from 'react';
 
export function TermlyScript() {
  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://app.termly.io/embed.min.js';
    script.setAttribute('data-auto-block', 'on');
    script.setAttribute('data-website-uuid', '3c9ac53b-dad2-4e9f-b46c-a3808e30f777');
    document.body.appendChild(script);
 
    return () => {
      // ⭐ Cleanup:component unmount 時移除
      document.body.removeChild(script);
    };
  }, []);
 
  return null;
}
tsx

重點:

  • 直接用原生 document.createElement,完全控制 attribute
  • setAttribute 不是 React setAttribute,是純 DOM API
  • cleanup 移除 避免 hot reload 時重複載入

放在 layout 最頂層

// app/layout.tsx
import { TermlyScript } from './TermlyScript';
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <TermlyScript />
        {children}
      </body>
    </html>
  );
}
tsx

TypeScript:.d.ts 宣告 window.displayPreferenceModal

Termly 載入後會在 window 加上方法,讓你可以從自己的網頁觸發 cookie 偏好彈窗:

window.displayPreferenceModal();
typescript

但 TS 不認識,build 會錯。要在 *.d.ts 補宣告:

// types/global.d.ts
declare global {
  interface Window {
    displayPreferenceModal: () => void;
  }
}
 
export {};   // ⭐ 沒這行 TS 會把整個檔案當 module
typescript
// 之後就能用
const handleCookieSettingsClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
  event.preventDefault();
  window.displayPreferenceModal();
};
 
<a href="#" onClick={handleCookieSettingsClick}>Cookie 偏好</a>
tsx
tsconfig.json
確保 tsconfig.json 的 include 涵蓋 *.d.ts:
{ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types/**/*.d.ts"] }
json

auto-block 機制

data-auto-block="on" 是 Termly 的核心:

  1. 自動掃描頁面 script tags
  2. 識別追蹤類 script(GA / FB Pixel / 廣告 SDK)
  3. 在使用者 同意之前 阻止它們執行
  4. 同意後才釋放

這個 magic 必須在 head 早期就執行,不然 GA 已經跑了再 block 沒意義。所以 Termly 應該放 <head> 最前面(不是 body 結尾):

useEffect(() => {
  const script = document.createElement('script');
  script.src = 'https://app.termly.io/embed.min.js';
  script.setAttribute('data-auto-block', 'on');
  script.setAttribute('data-website-uuid', '...');
  // ⭐ 改成 head insertBefore 第一個 child
  document.head.insertBefore(script, document.head.firstChild);
 
  return () => script.remove();
}, []);
tsx

但 useEffect 在 client mount 才跑,GA 等可能已經載過。更好的做法:在 Next.js <head> 直接寫 <script>:

// app/layout.tsx — App Router
export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <script
          src="https://app.termly.io/embed.min.js"
          data-auto-block="on"
          data-website-uuid="xxx"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}
tsx

Pages Router 用 _document.tsx:

// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';
 
export default function Document() {
  return (
    <Html>
      <Head>
        <script
          src="https://app.termly.io/embed.min.js"
          data-auto-block="on"
          data-website-uuid="xxx"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}
tsx

這個寫法 SSR 階段就把 script tag 寫進 HTML,瀏覽器收到 HTML 就立刻載入 → block 機制可以涵蓋所有後續 script。


替代方案

工具特色價格
TermlyUI 漂亮、政策產生器、auto-block免費 100K viewers / 月
Cookiebot業界老牌、合規完整$$
OneTrust企業級、最完整$$$
Iubenda義大利出身、多歐盟語言$
自己寫完全控制0(時間成本高)
個人 / 小型網站
Termly 免費額度夠用,而且 政策產生器幫你生 Privacy Policy / Cookie Policy / Terms of Service,省一週律師時間。

自己寫(若不想依賴 SaaS)

最簡 cookie banner:

'use client';
import { useEffect, useState } from 'react';
 
export function CookieBanner() {
  const [shown, setShown] = useState(false);
 
  useEffect(() => {
    const consent = localStorage.getItem('cookie-consent');
    if (!consent) setShown(true);
  }, []);
 
  const accept = () => {
    localStorage.setItem('cookie-consent', 'accepted');
    setShown(false);
    // 載入 GA / 其他 tracking
    loadAnalytics();
  };
 
  const decline = () => {
    localStorage.setItem('cookie-consent', 'declined');
    setShown(false);
  };
 
  if (!shown) return null;
 
  return (
    <div className="banner">
      <p>本站使用 Cookie 來改善使用者體驗。</p>
      <button onClick={accept}>同意</button>
      <button onClick={decline}>拒絕</button>
      <a href="/privacy">隱私政策</a>
    </div>
  );
}
tsx

但這只是 UI,不會自動 block 第三方 script。要全面合規還是建議用 Termly 等工具。


常見地雷

1. 沒同意就載 GA
Termly auto-block 有時會 miss,確認自己的 GA / 廣告 SDK 真的被 block:F12 → Network → 看是否有 google-analytics.com 請求。沒同意前不應該有。
2. 區域差異
美國訪客不需要彈窗,歐盟訪客需要。Termly 會 IP geo-locate 自動處理,但測試時可能看不到 banner(因為你在台灣 IP),要用 VPN 切歐盟測。
3. cleanup race condition
上面的 useEffect cleanup 在 hot reload 可能 Termly 仍掛在 window,移除 script 後 window.displayPreferenceModal 還在 → 但下次點會錯。可以加個 flag:
useEffect(() => {
  if (window.displayPreferenceModal) return;  // 已載過就 skip
  // ...
}, []);
typescript

目錄