Erscheinungsbild
0008 — Einwilligung vor Tracking, Basic Consent Mode als Vorgabe
Status: angenommen · Milestone: M5
Kontext
Die Kette ist vorgegeben: CookieConsent → Consent Mode v2 → Google Tag Manager → GA4 und optional Google Ads. Offen war, an welcher Stelle die Entscheidungen fallen und wie sich belegen lässt, dass sie richtig fallen.
Der teuerste Fehler in diesem Bereich ist unsichtbar. Lädt der Tag Manager eine Sekunde zu früh, sieht die Website genauso aus wie vorher — es wurden nur Daten übertragen, für die niemand eingewilligt hat. Niemand bemerkt es, bis jemand fragt. Im Referenzprojekt stand das GTM-Snippet aus Googles Vorlage unverändert im Kopf, samt <noscript>-iframe, und das Banner darüber war eine reine Anzeige ohne Wirkung.
Erschwerend kommt hinzu, dass sich das Verhalten nicht am Quelltext ablesen lässt: Ob eine Anfrage entsteht, hängt vom Zusammenspiel aus gespeichertem Cookie, Consent-Modus und Reihenfolge der Skripte ab.
Entscheidung
Der Ladeplan entsteht zur Bauzeit. planTracking() in src/lib/tracking/plan.ts gibt ein JSON-Objekt zurück, das alles enthält, wonach sich der Browser richtet: Modus, Kategorien, Signale, Ausgangszustand und — falls überhaupt — die Skript-Adresse des Tag Managers. Das Inline-Skript im Kopf trifft keine Entscheidung mehr, es führt aus. Dadurch ist das Verhalten ohne Browser prüfbar, und der Post-Build-Test vergleicht das ausgelieferte HTML mit der Ausgabe derselben Funktion.
Basic Consent Mode ist die Vorgabe. Vor einer Entscheidung entsteht keine einzige Verbindung zu einem Google-Server. Der Container wird erst geladen, wenn mehr als die notwendige Kategorie erlaubt wurde. tracking.consentMode: 'advanced' lädt ihn dagegen sofort mit verweigerten Signalen und überträgt dabei anonyme Messsignale — das muss ausdrücklich gewählt werden.
Der Schalter ist features.cookieConsent, nicht der Tag Manager. Die Consent-Schicht gehört nicht Google — eingebettete Videos, Karten oder ein Captcha brauchen dieselbe Einwilligung. Welche Kategorien der Dialog anbietet, entscheidet eine Registry in src/lib/tracking/consumers.ts: die Schnittmenge aus dem, was das Projekt in consent.categories erklärt, und dem, was es tatsächlich verbraucht. Heute ist GTM der einzige Verbraucher; künftige Features tragen sich dort ein, ohne dass die Weiche erneut angefasst wird.
Bleibt dabei nur die nicht abwählbare Kategorie übrig, erscheint der Dialog nur in development — als Vorschau zum Ansehen und Gestalten, mit einem Text, der genau das benennt.
Der Tag Manager lädt ausschließlich in production. Der Banner erscheint in jeder Umgebung, damit er sich vor dem Livegang ansehen und gestalten lässt. Es ist dieselbe Trennung wie bei der Indexierbarkeit (ADR 0006).
Kein <noscript>-iframe. Der Fallback des klassischen GTM-Snippets lädt bedingungslos und hat in einer Consent-Kette nichts zu suchen.
CookieConsent v3 von Orest Bida ist die einzige neue Abhängigkeit. Sie wird dynamisch nachgeladen; ihr Stylesheet ist über ?url eingebunden und wird nur von Seiten verlinkt, die tatsächlich fragen.
Begründung
Bauzeit statt Laufzeit. Eine Bedingung im Browser lässt sich nur im Browser prüfen, und Browser-Tests sind langsam und begrenzt. Steht die Entscheidung als Datenstruktur fest, deckt eine Handvoll Unit-Tests jede Kombination ab, und die End-to-End-Suite muss nur noch belegen, dass das Skript den Plan befolgt.
Basic statt Advanced. Der Advanced Mode misst mehr, überträgt dafür aber vor jeder Entscheidung Daten an Google. Ob das zulässig ist, hängt vom Einzelfall ab und ist nichts, was ein Starter für zwanzig Projekte vorwegnehmen darf. Die sichere Variante ist die Vorgabe; die andere ist eine bewusste Entscheidung mit einem Namen.
Das Update vor dem Container. Liegt bereits eine Entscheidung vor, wird sie im Inline-Skript sofort als consent update in den dataLayer geschrieben — vor dem Laden des Containers. Der Tag Manager spielt den dataLayer beim Start ab und sieht die Einwilligung dadurch, bevor das erste Tag feuert. Andernfalls liefe der erste Seitenaufruf mit verweigerten Signalen und ginge verloren.
Eine Sperre gegen doppeltes Laden. Der Container wird je Seitenaufruf höchstens einmal geladen, unabhängig davon, wie oft die Auswahl wechselt. Das ist die technische Fassung der Zusage „keine doppelten Seitenaufrufe".
hideFromBots: false. CookieConsent schaltet sich in der Voreinstellung ab, sobald navigator.webdriver gesetzt ist. Die End-to-End-Suite prüfte damit eine Seite, die es nie gibt. Für die Indexierung ändert sich durch das Abschalten nichts: Der Dialog entsteht erst im Browser und trägt data-nosnippet.
Die Fixture „full" benutzt basic, obwohl sie sonst alles einschaltet. consentMode ist kein Schalter für „mehr Funktion", sondern die Wahl zwischen zwei Netzwerkverhalten. Im Browser gefahren wird nur diese eine Ausgabe, und geprüft gehört das Verhalten, das jedes Kundenprojekt bekommt. Der Advanced Mode ist über die Unit-Tests des Plans abgedeckt; sein Unterschied im ausgelieferten HTML ist ein Feld im JSON.
Verworfene Alternativen
Das GTM-Snippet aus Googles Vorlage. Es lädt beim Parsen der Seite und bringt den <noscript>-iframe mit. Beides ist ohne Einwilligung unzulässig, und beides fällt niemandem auf.
Einen CMP-Dienst einkaufen (Usercentrics, Cookiebot). Laufende Kosten je Kundenprojekt, eine weitere Abhängigkeit zu einem fremden Server im kritischen Pfad, und die Bannertexte lägen außerhalb des Repositories. Was der Starter braucht — drei Kategorien, ein Widerruf, eine Fassungsnummer — ist der kleinste Teil dessen, was solche Dienste können.
Die Consent-Schicht selbst bauen. Der sichtbare Teil ist schnell geschrieben; der Rest — Fokusfalle im Dialog, Tastaturbedienung, Speicherformat, Fassungsverwaltung — ist es nicht. CookieConsent v3 ist dafür klein, ohne Abhängigkeiten und wird gepflegt.
Alle Bannertexte in die Projektkonfiguration. Sie wären dann in jedem Projekt neu zu schreiben. Stattdessen entstehen sie aus der Konfiguration: Der Abschnitt „Statistik" nennt Google Analytics genau dann, wenn tracking.ga4 gesetzt ist.
Den Banner fest an tracking.enabled hängen. War die erste Fassung und ist an zwei Stellen falsch: features.cookieConsent wäre ein Flag ohne Wirkung, und die Consent-Schicht gehörte damit dem Tag Manager — obwohl das nächste Feature, das eine Einwilligung braucht, kein Google-Produkt sein wird.
Den Banner in jeder Umgebung anzeigen, sobald das Flag steht. Ohne Verbraucher stünde in der Produktion ein Dialog, dessen Text keine Dienste benennen kann und dessen beide Schaltflächen dasselbe tun. Das ist keine Einwilligung, sondern eine Attrappe.
Konsequenzen
- Ein neues Tag im GTM-Container braucht eine Kategorie in
consent.categoriesund einen ehrlichen Satz im Einstellungsdialog. Beides ist Konfiguration, kein Code. - Ein neues Feature, das eine Einwilligung braucht, trägt sich in
consentConsumers()ein. Ohne diesen Eintrag erscheint seine Kategorie nicht im Dialog — und das ist die Absicht. - Ändert sich, wofür eingewilligt wird, muss
consent.revisionsteigen. Gespeicherte Entscheidungen zu einer älteren Fassung werden dadurch ungültig, und es wird erneut gefragt. - Der Advanced Mode wird nicht im Browser geprüft. Wer ihn einschaltet, prüft die Ausgabe von Hand — der Unterschied ist sichtbar am
wait_for_updateim Ladeplan. - Das Stylesheet von CookieConsent liegt als Datei in jeder Ausgabe, wird aber nur von Seiten mit Consent-Schicht verlinkt. Ein Post-Build-Test hält das fest.
- Der Starter ist damit technisch consent-ready. Ob ein konkretes Projekt datenschutzkonform ist, hängt an Konfiguration, Texten und rechtlicher Prüfung und wird hier nicht behauptet.