Enige tijd geleden hebben we, om te voldoen aan GDPR/ePrivacy, een lichtgewicht CMP (toestemmingsbeheertool) speciaal voor onze eigen website gebouwd. Meestal kies je hiervoor een SaaS-oplossing, maar in dit artikel vertellen we waarom we het zelf hebben gemaakt en hoe we dat hebben aangepakt.
Cookie-toestemming is steeds vaker onvermijdelijk voor websites die diensten aanbieden in het buitenland. De GDPR van de EU is het bekendste voorbeeld, maar ook in de Verenigde Staten neemt het aantal staatswetten op het gebied van privacy jaar na jaar toe. Metingen en advertentietags zonder toestemming van de bezoeker laten draaien is daardoor niet langer vanzelfsprekend.
In de EU verbieden de GDPR en de ePrivacy-richtlijn om vóór toestemming niet-essentiële cookies te plaatsen, zoals die van GA4 of Google Ads, of surfgegevens naar externe partijen te sturen. Het is ook niet toegestaan om het sluiten van een banner als toestemming te beschouwen, of om weigeren veel moeilijker te maken dan toestemming geven.
Deze regelgeving geldt ook voor ondernemingen buiten de EU, wanneer zij diensten aanbieden aan mensen in de EU of hun online gedrag volgen. Japanse websites die een Engelstalige versie hebben en internationaal actief zijn, of remarketingadvertenties inzetten, kunnen daardoor ook onder de regels vallen, ook zonder vestiging in de EU.
Daarnaast stelt Google eisen. Voor advertentiefuncties van Google voor bezoekers uit de EU is ondersteuning van Consent Mode v2 verplicht. Als de toestemmingsstatus niet correct naar Google wordt doorgegeven, werken meting en remarketing niet goed. In Japan is in 2023 met de herziene Telecommunicatiebedrijfswet de regeling voor 'externe verzending' ingegaan, waardoor bedrijven moeten openbaar maken welke gegevens naar welke diensten worden gestuurd.
Er is dus een systeem nodig dat aan de volgende drie eisen tegelijk voldoet.
- Het bepaalt de regio van de bezoeker en laat in de EU en andere gebieden geen enkele tag draaien totdat er toestemming is gegeven
- Het geeft de toestemmingsstatus correct door aan Google Consent Mode en biedt altijd een duidelijke manier om toestemming in te trekken
- Het publiceert een overzicht van externe verzendbestemmingen en cookies (beleid) en zorgt dat dit overeenkomt met het daadwerkelijke dataverkeer

Waarom we geen kant-en-klare cookiebanner-tool hebben gebruikt
Er zijn drie redenen.
- Conflicterende tags.
We wilden voorkomen dat een extern consentmanagementscript in conflict komt met GTM, advertentietags en SPA-navigatie, waardoor zelfs conversies van gebruikers die al toestemming hebben gegeven verloren gaan. - De te beheren tags liggen vast.
Onze metingen en advertenties draaien vrijwel volledig op Google-diensten (GTM / GA4 / Google Ads). Omdat we precies weten welke tags we moeten beheren, is de complexiteit van een generieke tool niet nodig. - Het past bij onze expertise.
Frontend-implementatie en toegankelijkheid zijn onze kernkwaliteiten. Een lichtgewicht, controleerbare oplossing die we zelf bouwen, levert precies de functies op die we nodig hebben.
Basisbeleid: in Japan melden, buiten Japan standaard weigeren
Zo hebben we de aanpak per regio bepaald.
Bezoeker | Beginstatus | Scherm | Google-tag |
|---|---|---|---|
Vanuit Japan | Toestaan | Geen banner | Start zoals gebruikelijk |
Buiten Japan | Weigeren | Banner onderaan het scherm (Alles toestaan / Alles weigeren / Instellingen). Na je keuze verschijnt een zwevende instellingenknop | Alleen starten na toestemming |
Onbeslist | Weigeren | Hetzelfde als buitenland | Alleen starten na toestemming |
GPC-signaal aanwezig | Alleen advertenties geweigerd | Reden tonen waarom advertenties zijn geweigerd | Gebruik voor advertenties gestopt |
In Japan volgen we de regels voor externe gegevensverzending: we baseren ons op kennisgeving en publicatie, en tonen geen banner. Voor het buitenland weigeren we in de beginstand zowel in de EU als in de VS. Veel Amerikaanse staatswetten werken met opt-out, dus dit kan in sommige gevallen strenger zijn dan nodig. We hebben er toch voor gekozen om het beheer eenvoudig te houden en de veilige kant op te gaan.
Ook weigeren wanneer niet kan worden vastgesteld of toestemming is gegeven, volgt dezelfde logica. Bij twijfel kiezen we ervoor de tags niet te activeren. Dit is het ontwerpprincipe dat door het hele systeem loopt, ook bij het gedrag na een mislukte laadpoging dat hieronder wordt beschreven.
Intrekken net zo eenvoudig maken als toestemming geven
De AVG vereist dat het intrekken van toestemming even eenvoudig is als het geven ervan. Als gebruikers via de banner met twee klikken toestemming kunnen geven, mag intrekken niet alleen mogelijk zijn via diep verborgen onderdelen van de privacypagina.
De knop "Cookie-instellingen" die het instellingenvenster opent, staat in alle regio's in de sectie "Toestemming wijzigen of intrekken" van de privacypagina. In de modus voor het buitenland tonen we bovendien na een keuze in de banner een zwevende instellingenknop linksonder in beeld, zodat wijzigen of intrekken vanaf elke pagina mogelijk is. In de modus voor Japan is er van nature geen banner, dus is er ook geen zwevende knop; alleen de knop op de privacypagina wordt getoond. Per project kan worden gekozen of ook in Japan een knop wordt getoond of niet.
Wanneer u uw toestemming intrekt, verwijderen we de Google-cookies die in het register staan en stoppen we met het verzenden van gegevens.
Opbouw: 3 onderdelen
[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] 各タグ側の「追加の同意チェック」で最終ガード
- Landdetectie ― Het land wordt bepaald op Edge, waarna een kenmerk aan de HTML wordt toegevoegd
- Toestemmingsscript ― Op basis van het kenmerk en de opgeslagen keuze wordt bepaald of de banner wordt getoond en of GTM wordt gestart
- Register ― Overzicht van de gebruikte externe diensten en cookies. Dit vormt de basis voor de beleidstabel en de lijst met te verwijderen cookies bij intrekking
① Landdetectie gebeurt op Edge, waarbij een kenmerk aan de HTML wordt toegevoegd
Landdetectie is met Cloudflare's request.cf.country eenvoudig te realiseren, maar de plaats waar dit gebeurt bepaalde het ontwerp.
Wanneer de browser zelf een detectie-API aanroept (een aanpak die veel grote internationale tools gebruiken), is het land pas bekend nadat het antwoord is ontvangen, en zien bezoekers uit Japan even een 'geweigerd'-status. Daarom hebben we gekozen voor een aanpak waarbij Edge de HTML herschrijft en <html>een regionaal kenmerk toevoegt. Met HTMLRewriter wordt alleen data-lcmp-region="jp" toegevoegd, zodat het script het land synchroon kan lezen.
// dist/_worker.js/index.js(サイト側の post-build で生成)
import inner from './astro-entry.js';
import { wrapWorker } from './lg-region-wrap.js';
export default wrapWorker(inner);
Onze site is gebouwd met Astro en @astrojs/cloudflare, en dat bracht de nodige uitdagingen met zich mee. Wanneer de adapter _worker.js genereert, wordt de map functions/ genegeerd, en pagina's die vooraf zijn gerenderd, passeren Astro's middleware evenmin. Uiteindelijk injecteren we de code door het toegangspunt van de na de build gegenereerde Worker in een wrapper te plaatsen.
Er wordt niet op taal gedetecteerd
We overwogen ook <html lang>... of navigator.language te gebruiken om dit te bepalen, maar als een bezoeker in de EU een Japanse pagina opent en dit verkeerd als jp herkend wordt, zou de tag activeren zonder toestemming. Dat is een fout aan de gevaarlijke kant.
For projects where a server-side check cannot be inserted (for example, on AWS or shared hosting, where DNS cannot be pointed to Cloudflare), we provide a method that queries our lookup endpoint by IP address. It follows the same "vendor-side lookup API" approach used by common consent management tools. The IP address and country code are not stored, only used for the lookup, and the response is limited to two values: jp or overseas. As a last resort, when none of these are available, there is also a mode that estimates the location from the device's time zone (for example, Asia/Tokyo maps to Japan).
You choose the detection method per site in the settings. Regardless of that setting, however, the script first checks the information that is available synchronously.
- Regional attribute embedded in the HTML (always used if Edge detection is present)
- Region stored in a cookie (for setups where the HTML cannot be rewritten and the server passes the detection result via a cookie)
- Only when neither 1 nor 2 is available, detection is performed using the method selected in the settings (IP lookup API or time zone)
- If the location still cannot be determined, the visitor is treated as overseas (the safe default).
The attribute and cookie are always given priority so that, if we later move to Edge detection, only the setting needs to change. Even if the IP lookup API times out, the visitor is simply treated as overseas, and tags never fire without consent.
② Before consent: not "blocked" but "not loaded"
Google's Consent Mode has two variants: Advanced, which loads GTM first and only sends a "denied" signal, and Basic, which does not load the GTM itself until consent is given. This implementation uses Basic. Structuring it so that only the consent script is read by GTM before consent keeps many things simple.
- Zelfs als het laden van het script mislukt, start de niet-essentiële tags niet, omdat er niemand is die GTM leest.
- Het GTM-fragment wordt niet rechtstreeks in de HTML geschreven. We kiezen ook niet voor een opzet waarbij het wordt aangeleverd als aangepaste HTML-tag in GTM, omdat GTM dan zou worden geladen vóór de toestemming.
- Daarnaast is in GTM bij elke tag de instelling "Alleen activeren bij toestemming voor analyse/advertenties" ingeschakeld, zodat er dubbele bescherming is.
"Vóór toestemming: 0 verzoeken, 0 cookies" wordt geverifieerd met E2E-tests op netwerkniveau.
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 件
});
Bij intrekking van toestemming worden alleen de bekende cookies uit het register verwijderd. Er wordt niet met jokertekens gezocht en verwijderd. Omdat we verwijderen wat we kennen, leidt een afwijkend Domain- of Path-veld in het register tot mislukte verwijdering. Dat de tests falen wanneer het register niet klopt, helpt om het privacybeleid consistent te houden met het daadwerkelijke verkeer.
③ Insluitingen buiten GTM (YouTube, kaarten, webfonts)
Het stoppen van GTM was niet voldoende. iframes van YouTube en Google Maps en webfonts die niet zelf gehost kunnen worden, worden rechtstreeks door de pagina geladen zonder via GTM te gaan, en plaatsen vóór toestemming cookies.
Hiervoor kozen we voor een aanpak waarbij één kenmerk in de HTML wordt herschreven.
<iframe data-lcmp-src="<https://www.youtube-nocookie.com/embed/ID>" data-lcmp-service="youtube" width="560" height="315"></iframe>
Vóór toestemming tonen we een tijdelijke weergave op basis van de waarden in het register ("Deze inhoud wordt geladen van YouTube; uw IP-adres en browsegegevens worden naar de aanbieder verzonden" + de knoppen "Alleen nu tonen" / "Altijd tonen"). Na toestemming laden we de echte inhoud.
We hebben geen "automatisch blokkeren" gebruikt, waarbij het DOM wordt bewaakt en laadpogingen vóór het laden automatisch worden herschreven. Dat leidt onvermijdelijk tot gemiste ladingen en weergaveproblemen, en is bovendien een functie waarvoor ook bij buitenlandse producten geen garantie wordt gegeven. Het herschrijven van één kenmerk per iframe kost wat moeite, maar werkt betrouwbaar.
Webfonts kunnen op dezelfde manier worden geblokkeerd. <link rel="stylesheet">Verander alleen het href-attribuut van de -tag in <head>data-lcmp-href; vóór toestemming wordt de CSS van het lettertype niet gelezen en pas na toestemming geladen. De loader wordt <link> synchroon uitgevoerd en verwerkt toegevoegde elementen direct. Als toestemming al is gegeven, wordt href direct na het invoegen van het element door de parser hersteld, waardoor het laadmoment van het lettertype vrijwel gelijk blijft aan de oorspronkelijke werking.
<link rel="stylesheet" data-lcmp-href="<https://fonts.googleapis.com/css2?family=…>" data-lcmp-service="google-fonts">
Als je toestemming verplicht stelt, worden lettertypen bij een eerste bezoek vanuit het buitenland niet geladen en wordt in plaats daarvan een systeemlettertype weergegeven. Wil je dat niet, dan is zelf hosten de juiste keuze: de lettertypebestanden staan dan op je eigen server, waardoor er helemaal geen externe verzending meer plaatsvindt en het vraagstuk daarmee ook vervalt.
Plak het in 1 regel, 8KB
Als voorwaarde geldt dat metingen- en advertentietags via GTM worden geladen. Websites die GTM niet gebruiken, kunnen niet zonder meer worden geïmplementeerd (eerst moeten de tags naar GTM worden verplaatst).
Aan uw kant hoeft u alleen <head>één synchronisatiescript bovenaan te plaatsen.
<script src="<https://cdn.liberogic.jp/lcmp/><siteId>/loader.js"></script>
De omvang van loader.js is ongeveer 8 KB gzipped. De UI voor banners en dialogen staat in een apart chunk en wordt pas geladen wanneer dat nodig is.
Daarnaast hebben we voor het laden van gtm.js in GTM de optie "na LCP + enkele seconden" toegevoegd. Wanneer deze is ingesteld, worden bezoekers die de site verlaten voordat het script is geladen niet meegeteld. Dit is een afweging, maar het beperkt wel de impact op de Lighthouse-score.
Uitgifte per site en beheerscherm
De configuratie (register, GTM ID, teksten en thema) wordt bij publicatie in de bundle ingebakken en via een CDN geleverd. Tijdens runtime wordt er geen configuratie-JSON opgehaald. Dit is zo ingericht om zowel "initialisatie is synchroon en zonder netwerkverkeer" als "het injecteren van willekeurige configuratie van buitenaf is verboden" te waarborgen. Een wijziging in de configuratie betekent dus opnieuw publiceren en opnieuw uitrollen. Bij elke publicatie blijft een versie bewaard, zodat u bij problemen naar de vorige versie kunt terugkeren.
De uitgave bevat loader.js / ui.js, plus kant-en-klare snippets, een implementatiehandleiding en een GTM-containersjabloon met vooraf ingestelde consentcontrole (voor import, in JSON). Dit om de kans te verkleinen dat in de GTM-omgeving van de klant tags zonder consentcontrole achterblijven.
Het beheerscherm maakt het mogelijk om deze publicatie uit te voeren zonder ontwikkelaarskennis. De belangrijkste functies zijn de volgende.
- Scheiding van concept en publicatie.
Het register is een juridische verklaring over externe gegevensoverdracht van elke site, dus wijzigingen worden niet meteen doorgevoerd bij het bewerken. Vóór publicatie toont het systeem een lijst met wijzigingen ten opzichte van de huidige instellingen in het Japans. Als het register is gewijzigd maar de datum van de privacyverklaring ongewijzigd is, verschijnt een waarschuwing. Via de publicatiegeschiedenis kunt u met één handeling terugkeren naar de vorige versie. - Catalogus met veelgebruikte diensten.
Diensten die vaak voorkomen op Japanse sites, zoals GA4, Google Ads, Clarity, YouTube en reCAPTCHA, kunnen met naam, aanbieder, doel en standaard cookiegegevens aan het register worden toegevoegd. De categorieën zijn voorstellen; de definitieve beoordeling ligt bij een mens. - Automatische cookiescan.
Doelpagina's worden in een echte browser geopend in twee toestanden: "vóór toestemming" en "alles toegestaan". De geplaatste cookies en externe verbindingen worden vergeleken met het register. Wat al vóór toestemming wordt geactiveerd, is "kritiek"; wat niet in het register staat, is "let op". Vanuit elke bevinding kan met één knop aan het register worden toegevoegd. - Automatisch weergegeven beleidstabel.
Plaats op de privacypagina alleen . De tabel wordt door loader.js opgebouwd uit het ingebakken register, zodat de beleidstabel en de daadwerkelijk beheerde tags nooit uit elkaar lopen. - Thema bewerken.
De kleuren van banners, dialogen en zwevende knoppen kunnen worden aangepast met een live voorbeeld. Het contrast wordt direct berekend: tekstkleur 4,5:1 en niet-tekstelementen zoals knoprandjes 3:1. Bij een tekort verschijnt een waarschuwing.
Conclusie
Cookiebeheer werkt pas goed als het niet alleen uit een banner bestaat, maar als vier onderdelen samenwerken: regionale detectie, tagbeheer, openbaarmaking van het beleid en een duidelijke manier om toestemming in te trekken. Met dit project hebben we dat samengebracht in één extern script en één register. Wijzigt u het register, dan veranderen tegelijk de bannertekst, de beleidstabel en de te verwijderen onderdelen bij intrekking. Er ontstaan dus geen plekken waar de gegevens uit elkaar lopen.
Voor sites die inkomsten genereren met advertenties (zoals AdSense) is een door Google erkende CMP verplicht als er advertenties in de EU worden getoond. Daarom valt dit niet onder die categorie. Voor algemene sites is deze oplossing echter ruim voldoende. Voor sites die vaste tags gebruiken en hun eigen toestemmingsprocessen willen kunnen controleren, is dit naar mijn mening een overzichtelijker alternatief dan een kant-en-klaar product.
Van DTP naar het web: al snel werkte hij aan markup, frontend, projectregie en accessibility, en beheerst hij alles. Actief bij Liberogic sinds de beginjaren en nu een levende encyclopedie binnen het bedrijf. Recentelijk verdiept hij zich, met prompts als hulpmiddel, in de vraag: "Kunnen we voor accessibility niet meer op AI leunen?" Technologie en denkwijze blijven zich ontwikkelen.
Ayumu Futamata
IAAP-gecertificeerd webaccessibility-specialist (WAS) / Markup-engineer / Frontend-engineer / Webdirecteur