首頁
學習紀錄
遊戲心得影視Life書單案件檔案
Side Projects委託作品與二創互動實驗場
Kurau
百百 BLOG
首頁
學習紀錄
遊戲心得影視Life書單案件檔案
Side Projects委託作品與二創互動實驗場
Kurau

Kurau Blog

「隨心而寫,真真假假,都是我」

一個記錄生活、輸出興趣的個人空間。
遊戲、影視、閱讀、學習……每一段體驗都值得留下文字。

頁面導覽

  • 學習紀錄
  • 遊戲心得
  • 影視Life
  • 書單
  • 委託作品與二創
  • Kurau
  • 合作邀請

找到我

歡迎來 Discord 找我聊天!

“曾經發生的事不可能忘記,只是暫時想不起來而已。”-《神隱少女》

© 2026 Kurau All rights reserved

資安

Termly 第三方cookie教學

By Kurau·2023-08-07·Updated 2026-05-09·6 分鐘閱讀

Termly 第三方 Cookie / 隱私政策整合(Next.js)

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

目錄

    ◆ 相關文章

    • cookie & sessionStorage加密 (crypto-js)

      2026-05-09
    • 保護你的API KEY (React 前端)

      2026-05-09
    • 限制特定網頁

      2026-05-09
    ← 上一篇Swiper 踩坑下一篇 →如何找到你的ip與local

    ◆ 關於作者

    Kurau

    個人寫作 / 創作的 SoT,記錄遊戲、影視、學習與生活。

    更多 Kurau 的文章