Erscheinungsbild
Consent
Die Einwilligung wird mit CookieConsent v3 von Orest Bida eingeholt — die einzige neue Abhängigkeit dieses Bereichs. Sie wird dynamisch nachgeladen: Auf einer Website ohne Consent-Schicht fordert der Browser weder das Skript noch das Stylesheet an.
Kategorien
Es gibt vier, festgelegt in consent.categories:
| Kategorie | Bedeutung | Consent-Mode-Signale |
|---|---|---|
necessary | Betrieb der Website; nicht abwählbar | security_storage, functionality_storage |
analytics | Reichweitenmessung | analytics_storage |
marketing | Werbewirkung und Remarketing | ad_storage, ad_user_data, ad_personalization, personalization_storage |
external-media | Eingebettete Inhalte fremder Anbieter | keine — läuft über den Dienst-Zustand von CookieConsent, nicht über Google |
external-media bietet der Dialog nur an, wenn consent.externalMedia mindestens einen Dienst nennt (key und name). Jeder Dienst erscheint als eigene Zeile; ExternalContentConsent am Einsatzort lädt die Einbettung erst nach der Freigabe — vorher steht sie nur als inertes <template> im Dokument, ohne iframe, Skript oder Anfrage. Ein Dienst, der am Einsatzort genannt wird, aber nicht in der Konfiguration steht, bricht den Build ab (Regel 10).
ad_user_data und ad_personalization sind die beiden Signale, die Consent Mode v2 gegenüber v1 ergänzt hat. Sie fehlen in vielen älteren Anleitungen — ohne sie behandelt Google die Einwilligung als unvollständig.
Ein Update nennt immer alle Signale, auch die verweigerten. Ein weggelassenes Signal ist für Google keine Ablehnung, sondern gar keine Angabe.
necessary ist im Ausgangszustand erteilt: Die Kategorie deckt genau die Speicherung ab, mit der die getroffene Entscheidung festgehalten wird. Ohne sie müsste bei jedem Seitenaufruf erneut gefragt werden.
Wer eine Kategorie verbraucht
Angeboten wird eine Kategorie nur, wenn hinter ihr etwas steht. Die Zuordnung steht in src/lib/tracking/consumers.ts und ist der Erweiterungspunkt der Consent-Schicht:
ts
export function consentConsumers(project: ProjectConfig): ConsentConsumer[] {
const verbraucher = [
{ key: 'consent-storage', name: 'Speicherung der Einwilligung', category: 'necessary' },
];
if (project.forms.provider !== 'none') {
verbraucher.push({ key: 'form-attribution', /* … */ category: 'marketing' });
}
if (project.tracking.enabled) {
// … alle optionalen Kategorien aus consent.categories
}
// Hier kommen künftige Verbraucher dazu: eingebettete Videos, Karten.
return verbraucher;
}Der key ist dabei nicht dekorativ: Die Bannertexte fragen darüber ab, was hinter einer Kategorie steht. Ein Vergleich auf name bräche, sobald jemand eine Formulierung verbessert.
Eine Kategorie kann mehrere Verbraucher haben
marketing ist der Fall, an dem das zuerst auftritt: Ein Projekt mit Formularen merkt sich die Kampagne, über die jemand hereingekommen ist — auch ganz ohne Tag Manager. Der Dialog bietet die Kategorie dann an, und der Text behauptet dort nichts über Google, weil nichts an Google geht. Sind beide Verbraucher da, nennt er beide. Siehe Formulare.
Der GTM-Container ist dabei eine Blackbox: Welche Tags darin liegen, weiß nur die GTM-Konfiguration. consent.categories ist die Erklärung darüber, und sie gilt. tracking.ga4 und tracking.googleAds schränken die Liste nicht ein — sie benennen zusätzlich, was konkret dahintersteht, und schärfen dadurch die Texte. Andersherum wäre es eine Falle: Ein Container mit einem anderen Statistik-Tag verlöre seine Kategorie und würde nie geladen.
Ein Dialog ohne abwählbare Kategorie ist keine Frage
Bleibt nach dieser Rechnung nur necessary übrig, hätte der Dialog zwei gleichbedeutende Schaltflächen und einen Text, der nichts Wahres sagen kann. Er erscheint dann nur in development — als Vorschau zum Gestalten, mit einem Text, der genau das benennt.
Was der dataLayer sieht
js
// vor allem anderen, im Kopf der Seite
gtag('consent', 'default', { ad_storage: 'denied', /* … */, security_storage: 'granted' });
// nach der Entscheidung — oder sofort, wenn schon eine gespeichert ist
gtag('consent', 'update', { analytics_storage: 'granted', /* … */ });
dataLayer.push({ event: 'consent_update', consent_categories: ['necessary', 'analytics'] });Das Ereignis consent_update ist der Auslöser, an dem sich Tags im Container aufhängen lassen, die erst nach einer Entscheidung feuern sollen.
Texte
Die Texte stehen in src/lib/tracking/texts.ts und leiten sich aus der Konfiguration ab: Der Abschnitt „Statistik" nennt Google Analytics 4 genau dann, wenn tracking.ga4 gesetzt ist, der Abschnitt „Marketing" nennt Google Ads genau dann, wenn tracking.googleAds gesetzt ist. Sonst bleiben sie allgemein — dann liegt im Container etwas anderes, und der Text soll nichts Genaueres behaupten.
Der Abschnitt „Marketing" wird darüber hinaus aus den tatsächlichen Verbrauchern zusammengesetzt. Der Satz „Dabei werden Daten an Google übertragen" erscheint nur, wenn es einen Tag Manager gibt; in einem Projekt mit Formularen, aber ohne Tracking wäre er schlicht falsch — und eine Einwilligung, die auf einer falschen Angabe beruht, ist keine.
Der Verweis auf Datenschutzerklärung und Impressum kommt aus navigation.legal.
Nur deutsche Texte
Der Starter liefert ausschließlich deutsche Texte. Steht site.locale auf einer anderen Sprache, bricht der Build ab und nennt die Stelle. Eine weitere Sprache kommt als Eintrag in TRANSLATIONS dazu — ein Banner in der falschen Sprache ist rechtlich wertlos, weil niemand einwilligt, was er nicht liest.
Widerruf
Der Schalter Cookie-Einstellungen steht im Seitenfuß jeder Seite und zusätzlich auf der Datenschutzseite. Er trägt das Attribut data-cc="show-preferencesModal"; CookieConsent verbindet sich selbst damit. Ein eigener Klick-Handler ist nicht nötig — und ein solcher Schalter erscheint nur, wenn es eine Einwilligung gibt.
Wird eine Kategorie zurückgenommen, löscht CookieConsent die zugehörigen Cookies: _ga* und _gid für Statistik, _gcl* und _gac* für Marketing. Cookies auf fremden Domains — etwa IDE auf doubleclick.net — lassen sich vom Browser aus nicht löschen; verhindert wird stattdessen, dass sie überhaupt gesetzt werden.
Fassungsnummer
consent.revision ist die Fassung, zu der eine Einwilligung erteilt wurde. Wird sie erhöht, sind gespeicherte Entscheidungen ungültig: Der Banner erscheint erneut, und bis zur neuen Entscheidung lädt nichts.
Wann die Fassungsnummer steigen muss
Immer dann, wenn sich ändert, wofür eingewilligt wird — ein neues Tag im Container, eine neue Kategorie, ein neuer Dienst. Nicht bei einer Textkorrektur.
Die Entscheidung wird im Cookie cc_cookie gespeichert, 182 Tage lang. Sechs Monate sind der in Deutschland gängige Richtwert: lang genug, um nicht bei jedem Aufruf zu fragen, kurz genug, um eine Einwilligung nicht unbegrenzt fortzuschreiben.
Gestaltung
src/styles/consent.css legt die CSS-Variablen von CookieConsent auf die Design Tokens des Projekts. Es werden keine Regeln der Bibliothek überschrieben — nur ihre Variablen gesetzt. Dadurch trägt der Banner die Farben des Kunden, ohne dass ein Selektor nachgebaut wird, der beim nächsten Paket-Update bricht.
Zwei Festlegungen sind bewusst und sollten nicht ohne Grund geändert werden:
- Die Seite wird nicht gesperrt. Wer nicht entscheiden will, kann trotzdem lesen. Eine erzwungene Auswahl ist keine freiwillige Einwilligung.
- Beide Schaltflächen sind gleich gewichtet. „Nur notwendige" darf nicht schwerer zu finden sein als „Alle akzeptieren".
Banner und Einstellungsdialog werden bei jedem pnpm verify mit axe geprüft — siehe Was geprüft ist.