Termly 第三方cookie教學
Termly 第三方 Cookie / 隱私政策整合(Next.js)
TL;DRTermly 是 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"
/>
問題:
- 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;
}
重點:
- 直接用原生
document.createElement,完全控制 attribute setAttribute不是 ReactsetAttribute,是純 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>
);
}
TypeScript:.d.ts 宣告 window.displayPreferenceModal
Termly 載入後會在 window 加上方法,讓你可以從自己的網頁觸發 cookie 偏好彈窗:
window.displayPreferenceModal();
但 TS 不認識,build 會錯。要在 *.d.ts 補宣告:
// types/global.d.ts
declare global {
interface Window {
displayPreferenceModal: () => void;
}
}
export {}; // ⭐ 沒這行 TS 會把整個檔案當 module
// 之後就能用
const handleCookieSettingsClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
window.displayPreferenceModal();
};
<a href="#" onClick={handleCookieSettingsClick}>Cookie 偏好</a>
tsconfig.json確保 tsconfig.json 的 include 涵蓋 *.d.ts:{ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types/**/*.d.ts"] }
auto-block 機制
data-auto-block="on" 是 Termly 的核心:
- 自動掃描頁面 script tags
- 識別追蹤類 script(GA / FB Pixel / 廣告 SDK)
- 在使用者 同意之前 阻止它們執行
- 同意後才釋放
這個 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();
}, []);
但 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>
);
}
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>
);
}
這個寫法 SSR 階段就把 script tag 寫進 HTML,瀏覽器收到 HTML 就立刻載入 → block 機制可以涵蓋所有後續 script。
替代方案
| 工具 | 特色 | 價格 |
|---|---|---|
| Termly | UI 漂亮、政策產生器、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>
);
}
但這只是 UI,不會自動 block 第三方 script。要全面合規還是建議用 Termly 等工具。
常見地雷
1. 沒同意就載 GATermly 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 // ... }, []);