這是不久前的事,為了因應 GDPR/ePrivacy,我們自行開發了一套專用於自家網站、輕量的 CMP(同意管理工具)。一般來說會直接導入 SaaS,但這次想分享「為什麼要自己做」以及「怎麼做」。
首先談 Cookie 同意。對於面向海外提供服務的網站來說,這已經是無法迴避的課題。歐盟的 GDPR 最具代表性,而美國各州的隱私法也逐年增加,「未經同意就啟動分析或廣告標籤」已不再是理所當然的做法。
在歐盟,依據 GDPR 與 ePrivacy 指令,在取得同意之前,不得放置 GA4 或 Google 廣告等非必要 Cookie,也不得將瀏覽資料傳送至外部。將「關閉」視為同意,或是讓撤回同意比給予同意更麻煩,同樣是不合規的做法。
此規範同樣適用於歐盟境外的業者,只要其服務針對歐盟境內的人提供,或追蹤歐盟境內人士的行為即可。例如,製作英文版並向海外提供服務、或投放再行銷廣告的日本網站,即使在歐盟沒有營業據點,也可能適用。
Google 的要求也隨之增加。若要在歐盟地區的訪客身上使用 Google 廣告功能,必須支援 Consent Mode v2,且若未正確向 Google 傳達同意狀態,衡量與再行銷將無法正常運作。在國內,2023 年修正的《電信事業法》也已開始實施「外部傳送規範」,要求公開哪些服務會接收哪些資訊。
也就是說,我們需要同時滿足以下三項的機制。
- 判定訪客所在地區,在歐盟等地區於取得同意前,完全不啟動任何標籤
- 將同意狀態正確傳達給 Google Consent Mode,並提供隨時可撤回同意的途徑
- 公開外部傳送對象與 Cookie 清單(政策),並持續確保其與實際通訊內容一致

為何不採用現成的同意管理工具
原因有三點。
- 標籤衝突。
我們希望避免外部同意管理腳本與 GTM、廣告標籤及 SPA 頁面切換產生衝突,以免已同意的使用者連轉換數據都遺失 - 控制對象可以固定。
我們使用的衡量與廣告工具幾乎都屬於 Google 系列(GTM / GA4 / Google Ads)。需要控制的標籤已經確定,因此不需要通用工具的複雜功能 - 符合我們的專長。
前端實作與無障礙設計正是我們的強項,自行打造輕量且可稽核的機制,能只納入所需功能
基本方針:日本採通知制,海外採預設拒絕
各地區的運作方式如下。
訪客 | 初始狀態 | 畫面 | Google 標籤 |
|---|---|---|---|
來自日本 | 允許 | 無橫幅 | 依原樣啟動 |
來自日本以外 | 拒絕 | 畫面下方顯示橫幅(全部允許/全部拒絕/詳細設定)。選擇後會出現浮動的設定按鈕 | 僅在同意後啟動 |
無法判定 | 拒絕 | 與海外相同 | 僅在同意後啟動 |
偵測到 GPC 訊號 | 僅拒絕廣告 | 顯示廣告遭拒絕的原因 | 停止廣告用途 |
日本依循「外部傳送規範」,以通知與公告為基本,不顯示橫幅。海外方面,歐盟與美國一律將初始狀態設為拒絕。美國多數州法以事後退出(opt-out)為主,因此這樣的做法可能略顯嚴格,但為了簡化營運,我們採取偏向安全的統一設定。
判定無法完成時也採取拒絕,理由相同。若有疑慮,就傾向「不變動標籤」。這是貫穿整體的設計理念,後文提到的「讀取失敗時的行為」也都依循此方向。
撤回的操作路徑應與同意「一樣簡單」
GDPR 要求撤回同意的難易度,應與同意時相同。若在橫幅中兩次點擊即可同意,撤回卻只能從隱私權政策頁面的深處操作,這樣就不對等了。
開啟設定對話框的「Cookie 設定」按鈕,無論地區為何,都放在隱私權政策頁面的「變更與停止同意的方式」段落中。海外模式除此之外,還會在使用者於橫幅中做出選擇後,於畫面左下角常駐一個浮動設定按鈕,讓使用者可在任何頁面變更或撤回同意。日本模式原本就不顯示橫幅,因此不放置浮動按鈕,僅保留隱私權政策頁面的按鈕。依專案需求,也可切換為「日本也顯示」或「不顯示」。
撤回後,將刪除台帳中列出的 Google 相關 Cookie,並停止後續傳送。
結構:共 3 個部分
[Edge] Cloudflare Worker が request.cf.country を見て
<html data-lcmp-region="jp|overseas"> を HTML に注入
[HTML] <head> 先頭の同期スクリプト(loader.js)が
地域・保存済みの選択・GPC から gtag('consent','default') を確定
→ 日本 or 有効な同意あり → GTM を読む
→ それ以外 → GTM を読まず、バナー UI を遅延読み込み
[UI] 選択 → 検証・保存 → gtag('consent','update') → GTM を読み込む(何度呼ばれても挿入は1回だけ)
[GTM] 各タグ側の「追加の同意チェック」で最終ガード
- 地區判定 ― 在 Edge 判定國家,並在 HTML 上標記
- 同意腳本 ― 依據標記與已儲存的選擇,決定是否顯示橫幅並啟動 GTM
- 台帳 ― 所使用外部服務與 Cookie 的清單。作為政策表的依據,也是撤回時刪除對象的原始資料
① 地區判定於 Edge 在 HTML 上標記
國家判定其實可以用 Cloudflare 的 request.cf.country 完成,但關鍵在於要在哪裡使用。
若採用由瀏覽器端呼叫判定 API 的方式(海外大型工具多採用此做法),在收到回應前都無法得知地區,日本訪客也會短暫處於「拒絕」狀態。因此我們改為在 Edge 改寫 HTML,於<html>前填入地區屬性。只需透過 HTMLRewriter 加上 data-lcmp-region="jp",腳本便能同步讀取地區。
// dist/_worker.js/index.js(サイト側の post-build で生成)
import inner from './astro-entry.js';
import { wrapWorker } from './lg-region-wrap.js';
export default wrapWorker(inner);
本公司網站採用 Astro 搭配 @astrojs/cloudflare,過程中遇到一些難題。在 adapter 會產生 _worker.js 的架構下,functions/ 目錄會被忽略,而且預先渲染完成的頁面也不會經過 Astro 的 middleware。最後我們以包裝器包住建置後產生的 Worker 入口,藉此注入相關邏輯。
不依語言進行判定
我們也考慮過透過 <html lang>... 或 navigator.language 來判定,但如果歐盟居住者開啟日文頁面時被誤判為 jp,就會在未經同意的情況下觸發標籤,這屬於風險較高的錯誤。
針對無法在伺服器端插入判斷的專案(例如使用 AWS 或租用伺服器,且無法將 DNS 指向 Cloudflare 的架構),我們提供了透過 IP 位址查詢本公司判斷端點的方式。這與一般同意管理工具採用的「供應商端判斷 API」做法相同。判斷時僅用於查詢,不會儲存 IP 或國家代碼,回應也只有 jp 或 overseas 兩種值。若上述方式皆無法使用,最後的備案是依裝置的時區(例如 Asia/Tokyo 即判定為日本)進行推定。
判斷方式可依各網站的設定選擇。不過,無論設定為何,腳本都會先檢查可同步取得的資訊。
- 嵌入 HTML 中的地區屬性(若有 Edge 判斷,一律優先使用此項)
- 寫入 Cookie 的地區資訊(適用於無法改寫 HTML、由伺服器端將判斷結果寫入 Cookie 的架構)
- 僅在上述 1、2 皆無資訊時,才依設定所選的方式(IP 判斷 API 或時區)進行判斷
- 若仍無法取得資訊,則視為海外(**採安全側處理**)
優先採用屬性與 Cookie,是為了日後改為 Edge 判斷時,只需變更設定即可。即使 IP 判斷 API 逾時,也只會落入海外處理,不會在未取得同意的情況下載入追蹤代碼。
② 同意前不是「封鎖」,而是「不讀取」
Google 的 Consent Mode 有兩種模式:Advanced 會先載入 GTM,僅傳送「拒絕」訊號;Basic 則在取得同意前完全不載入 GTM 本體。本次採用 Basic。若在同意前載入 GTM 的只有同意腳本,整體結構會變得簡單許多。
- 即使腳本載入失敗,由於沒有人會讀取 GTM,非必要的標籤也不會啟動。
- 不會將 GTM 程式碼片段直接寫入 HTML,也不採用以 GTM 自訂 HTML 標籤發佈的方式(因為這樣會在使用者同意之前就讀取 GTM)。
- 另外在 GTM 端,每個標籤也設定「沒有分析/廣告同意就不觸發」的勾選項目,雙重防護。
「同意前通信 0 次、Cookie 0 個」已透過 E2E 測試在網路層級驗證。
const FORBIDDEN_BEFORE_CONSENT = [
'**googletagmanager.com/**', '**google-analytics.com/**',
'**googleadservices.com/**', '**doubleclick.net/**', // …
];
test('T02: overseas first visit sends nothing', async ({ page }) => {
const hits: string[] = [];
await page.route('**/*', route => {
if (FORBIDDEN_BEFORE_CONSENT.some(p => matchGlob(p, route.request().url()))) hits.push(route.request().url());
return route.continue();
});
await page.goto('/');
await expect(page.getByRole('region', { name: /cookie/i })).toBeVisible();
expect(hits).toEqual([]); // 通信 0 件
expect(await page.context().cookies()).toEqual([]); // Cookie 0 件
});
撤回同意時,僅刪除台帳中列舉的已知 Cookie,不會以萬用字元方式去清除。採用「刪除已掌握的項目」的做法,因此若台帳的 Domain / Path 與實際情況不符,刪除就會失敗。為了讓政策表與實際通信持續保持一致,我們設計成台帳不正確時測試就會失敗,這點發揮了很大作用。
③ GTM 之外的嵌入內容(YouTube、地圖、網頁字型)
停用 GTM 並不代表問題就全部解決。YouTube 與 Google 地圖的 iframe,以及無法自行託管的網頁字型,都會不經過 GTM、直接由頁面載入,並在使用者同意之前就寫入 Cookie。
為此,我們採用在 HTML 端只需改寫一處屬性的方式。
<iframe data-lcmp-src="<https://www.youtube-nocookie.com/embed/ID>" data-lcmp-service="youtube" width="560" height="315"></iframe>
在使用者同意之前,會顯示根據台帳內容產生的預留位置(「此內容將從 YouTube 載入,您的 IP 位址與瀏覽資訊將傳送給提供者」+「僅此次顯示」/「一律顯示」按鈕),並在獲得許可後才載入實際內容。
我們沒有採用透過監控 DOM、在載入前自動改寫的「自動封鎖」方式。這種做法難以避免載入遺漏與版面錯位,而且屬於海外產品也不保證運作的功能。雖然需要手動改寫 iframe 的一處屬性,但能確實運作。
網頁字型也可以用同樣的機制阻擋。只要將 <link rel="stylesheet"> 的 href 改為 data-lcmp-href,同意前就不會讀取字型 CSS,獲得許可後才會載入。載入器以 <head>同步 方式執行,並即時處理新增的元素;因此在已獲許可的情況下,解析器插入 <link> 後會立即還原 href,字型的載入時機與原本幾乎相同。
<link rel="stylesheet" data-lcmp-href="<https://fonts.googleapis.com/css2?family=…>" data-lcmp-service="google-fonts">
不過,採用同意制後,來自海外的首次造訪將不會載入字型,改以系統字型顯示。若不希望如此,最佳做法是將字型檔案放在自己的伺服器上(自架),如此就不再有對外傳送的問題,這個爭議也隨之消失。
只需貼上 1 行,8KB。
前提是計測與廣告代碼已透過 GTM 運作。未使用 GTM 的網站無法直接導入(需要先將代碼遷移至 GTM)。
導入端的作業只需在 <head> 的最前面放置一支同步腳本。
<script src="<https://cdn.liberogic.jp/lcmp/><siteId>/loader.js"></script>
大小方面,loader.js 經 gzip 壓縮後約為 8KB。橫幅與對話框的 UI 則分開成另一個區塊,僅在需要時才延遲載入。
此外,GTM 的 gtm.js 插入還加入了「LCP 之後 + 數秒」的選項。啟用後,在插入前就離開的訪客將不會被計測,這是一項取捨,但可以減少對 Lighthouse 分數的影響。
依網站區分的發佈與管理後台
設定(台帳、GTM ID、文案、主題)會在發佈時打包進 bundle,並由 CDN 提供。執行時不會另外讀取設定 JSON。這是為了同時滿足「初始化為同步且不進行通訊」與「禁止從外部任意注入設定」,因此變更設定即等於重新發佈與重新配送。每次發佈都會保留版本紀錄,如有問題可回復至前一版本。
發佈內容包含 loader.js / ui.js,以及即貼即用的程式片段、導入說明文件,還有已完成同意檢查設定的 GTM 容器範本(匯入用 JSON)。這是為了減少導入端出現「仍保留未經同意檢查的代碼」這類疏失。
這是讓非開發人員也能進行發布作業的管理畫面。主要功能如下。
- 草稿與發布分離。
台帳是關於各網站對外傳送資料的法律聲明,因此不會在編輯後立即生效。發布前會以日文變更清單顯示與「目前已發布設定」的差異;若台帳已變更但政策修訂日未更新,會發出警告。也可以從發布歷程一鍵還原至前一個版本。 - 常用服務目錄。
可將國內網站常見的 GA4、Google 廣告、Clarity、YouTube、reCAPTCHA 等服務加入台帳,並附上名稱、提供者、用途及 Cookie 的標準資訊。分類為建議值,最終判斷仍由人員決定。 - Cookie 自動掃描。
會以實際瀏覽器開啟目標頁面,分別在「同意前」與「全部允許」兩種狀態下檢視,並將設置的 Cookie 與連線目的地與台帳進行比對。若在同意前已觸發,標示為「重大」;若台帳中沒有記載,標示為「注意」。可從檢查結果一鍵加入台帳。 - 政策表自動產生。
只需在政策頁面放置 。表格由 loader.js 內嵌的台帳產生,因此政策表與實際控制對象不會產生落差。 - 主題編輯。
可一邊預覽實際畫面,一邊調整橫幅、對話框及浮動按鈕的配色。系統會即時計算文字顏色的 4.5:1 及按鈕邊框等非文字元素的 3:1 對比度,若不足則發出警告。
總結
Cookie 同意管理並非「顯示橫幅就結束」,必須讓地區判斷、標籤控制、政策公開與撤回途徑這四項環環相扣,才能真正成立。這次我們將這些功能整合為一支外部腳本與一份台帳。只要修改台帳,橫幅說明、政策表與撤回時的刪除對象會同時更新,不會出現不一致的地方。
若網站需透過刊登廣告獲利(如 AdSense 等),且要在歐盟地區投放廣告,則必須採用 Google 認證的 CMP,因此不在本工具的適用範圍內;但對一般網站而言,本工具已綽綽有餘。對於已確定使用的標籤、並希望自行稽核同意機制的網站來說,相較於導入現成工具,這應是更清楚透明的選擇。
從 DTP 踏入網頁世界,不知不覺間,標記、前端、專案指導與無障礙,樣樣精通的「技藝仙人」。自 Liberogic 創業初期就跨領域活躍,如今是公司內部的活字典般的存在。最近沉迷於運用提示詞提升效率的探索,想著「無障礙相關的工作,能不能更依賴 AI 呢?」技術與思考,仍在持續進化中
二俣 步
IAAP認證網頁無障礙專員(WAS) / 標記工程師 / 前端工程師 / 網站總監