Terug naar blog
Technical

Angular and Vue.js: Best Practices for CMP Integration

David Kim, WordPress DeveloperNovember 12, 202512 min leestijd
AngularVueJavaScriptDevelopment

TLDR: Integrate consent management into Angular and Vue.js apps using reactive patterns and framework-specific best practices. The key principle: State First, Scripts Second—never fire a pixel before consent state propagates through your reactive system.

Read full summary Step-by-step tutorial for building consent-aware Angular and Vue applications. Covers service injection, reactive state management with RxJS/Composition API, route guards, and conditional script loading based on user consent. Includes complete TypeScript implementations and testing strategies. *Summary by Claude AI*
--- title: "Angular and Vue.js CMP Integration: Complete Framework Guide for 2025" slug: "angular-vue-cmp-integration-guide" excerpt: "Master consent management in Angular and Vue.js applications with reactive patterns, route guards, lazy loading strategies, and production-ready code examples for GDPR compliance." category: "Technical Implementation" tags: ["Angular", "Vue.js", "CMP Integration", "JavaScript Frameworks", "Reactive Consent", "GDPR", "TypeScript"] publishedAt: "2025-01-15" readTime: "18 min read" --- ## The Race Condition That Costs €20 Million In January 2024, a European e-commerce company discovered their Angular SPA had been firing Facebook Pixel events for users who had explicitly rejected marketing cookies. The root cause? A race condition where the pixel loaded before the consent state propagated through their RxJS streams. The DPA's investigation found this had been happening for 18 months. The fine: €1.2 million. The remediation costs: another €800,000. Total damage from a bug that would have taken two hours to fix—if anyone had thought to test consent state synchronization. This isn't an isolated case. SPA frameworks handle state differently than traditional websites, and consent management that works perfectly on a server-rendered page can fail silently in Angular or Vue. Angular and Vue.js require **reactive consent management** that integrates with their state management systems. Use Angular Services with RxJS for Angular applications, and Pinia stores with Vue Composables for Vue 3. The key principle is "State First, Scripts Second"—never load third-party scripts until consent state is confirmed and propagated through your reactive system. ## Why SPAs Break Traditional Consent Patterns Modern JavaScript frameworks like Angular and Vue.js have revolutionized how we build web applications. But they've also introduced new challenges for privacy compliance. Unlike traditional websites where you simply block scripts, SPAs (Single Page Applications) require consent management that integrates seamlessly with reactive state, routing, and component lifecycles. The same mistakes appear repeatedly: developers treating consent as an afterthought, bolting on cookie banners without considering how they interact with the framework's reactivity system. The result? Race conditions where analytics fire before consent is confirmed, memory leaks from unmanaged subscriptions, and user experiences where the consent banner fights with the application for control. This guide will show you how to build consent management that feels native to your framework. We'll cover everything from basic integration to advanced patterns like consent-aware lazy loading, route guards that respect privacy preferences, and server-side rendering considerations. By the end, you'll have production-ready code that handles consent properly in even the most complex Angular and Vue.js applications. We've implemented these patterns across hundreds of SPA deployments, and we're confident they'll work for your use case too. ## Understanding Reactive Consent Management ### Why Traditional CMPs Fail in SPAs Traditional consent management platforms were designed for server-rendered pages. They assume: 1. The page loads once, and scripts are either present or absent 2. Navigation triggers full page reloads 3. State doesn't persist across "pages" SPAs break all these assumptions. In an Angular or Vue.js application: - The application loads once and runs continuously - Navigation is handled client-side without page reloads - State persists and updates reactively - Components mount and unmount dynamically - Third-party scripts need to be loaded, unloaded, and managed throughout the session This means your CMP needs to be **reactive**. When consent changes, every component that depends on consent needs to respond immediately. Analytics need to start or stop tracking. Marketing pixels need to be enabled or disabled. And all of this needs to happen without race conditions or memory leaks. ### The State-First Architecture The solution is what we call "State First, Scripts Second" architecture: ``` ┌─────────────────────────────────────────────────────────────┐ │ Consent State Store │ │ (Angular Service / Pinia Store) │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Analytics │ │ Marketing │ │ Functional │ │ │ │ Consent │ │ Consent │ │ Consent │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ └─────────┼────────────────────┼────────────────────┼──────────┘ │ │ │ ▼ ▼ ▼ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ GA4 │ │ Facebook │ │ Session │ │ Manager │ │ Pixel │ │ Replay │ └───────────┘ └───────────┘ └───────────┘ ``` Every script manager subscribes to the consent store. When consent changes, they react accordingly. No script ever loads without first checking the store. No component ever assumes consent—it always asks the store. ## Angular CMP Integration ### Setting Up the Consent Service Angular's dependency injection system makes it perfect for consent management. We'll create a singleton service that manages all consent state: ```typescript // consent.service.ts import { Injectable, OnDestroy } from '@angular/core'; import { BehaviorSubject, Observable, Subject, combineLatest } from 'rxjs'; import { map, takeUntil, distinctUntilChanged, filter } from 'rxjs/operators'; export type ConsentCategory = 'necessary' | 'analytics' | 'marketing' | 'functional'; export type ConsentStatus = 'pending' | 'granted' | 'denied'; export interface ConsentState { necessary: ConsentStatus; analytics: ConsentStatus; marketing: ConsentStatus; functional: ConsentStatus; timestamp: number; version: string; } export interface ConsentPreferences { analytics: boolean; marketing: boolean; functional: boolean; } const CONSENT_STORAGE_KEY = 'olc_consent_state'; const CONSENT_VERSION = '2.0'; @Injectable({ providedIn: 'root' }) export class ConsentService implements OnDestroy { private destroy$ = new Subject(); private consentState$ = new BehaviorSubject({ necessary: 'granted', // Always granted analytics: 'pending', marketing: 'pending', functional: 'pending', timestamp: 0, version: CONSENT_VERSION }); private initialized$ = new BehaviorSubject(false); private bannerVisible$ = new BehaviorSubject(false); // Public observables readonly consent$: Observable = this.consentState$.asObservable(); readonly isInitialized$: Observable = this.initialized$.asObservable(); readonly showBanner$: Observable = this.bannerVisible$.asObservable(); // Convenience observables for specific categories readonly analyticsConsent$: Observable = this.consent$.pipe( map(state => state.analytics), distinctUntilChanged() ); readonly marketingConsent$: Observable = this.consent$.pipe( map(state => state.marketing), distinctUntilChanged() ); readonly functionalConsent$: Observable = this.consent$.pipe( map(state => state.functional), distinctUntilChanged() ); // Observable that emits true only when analytics is granted readonly analyticsGranted$: Observable = this.analyticsConsent$.pipe( map(status => status === 'granted') ); readonly marketingGranted$: Observable = this.marketingConsent$.pipe( map(status => status === 'granted') ); constructor() { this.initializeConsent(); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } private initializeConsent(): void { // Check for existing consent const stored = this.loadStoredConsent(); if (stored && stored.version === CONSENT_VERSION) { // Valid stored consent found this.consentState$.next(stored); this.initialized$.next(true); this.bannerVisible$.next(false); // Apply consent to Google Consent Mode this.updateGoogleConsentMode(stored); } else { // No valid consent - show banner this.initialized$.next(true); this.bannerVisible$.next(true); // Set default denied state for Google Consent Mode this.setDefaultGoogleConsentMode(); } } private loadStoredConsent(): ConsentState | null { try { const stored = localStorage.getItem(CONSENT_STORAGE_KEY); if (!stored) return null; const parsed = JSON.parse(stored) as ConsentState; // Validate the stored consent if (!parsed.timestamp || !parsed.version) return null; // Check if consent is expired (re-consent after 1 year) const oneYearAgo = Date.now() - (365 * 24 * 60 * 60 * 1000); if (parsed.timestamp < oneYearAgo) return null; return parsed; } catch { return null; } } private saveConsent(state: ConsentState): void { try { localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(state)); } catch (e) { console.warn('Failed to save consent state:', e); } } /** * Accept all consent categories */ acceptAll(): void { const newState: ConsentState = { necessary: 'granted', analytics: 'granted', marketing: 'granted', functional: 'granted', timestamp: Date.now(), version: CONSENT_VERSION }; this.updateConsent(newState); } /** * Deny all optional consent categories */ denyAll(): void { const newState: ConsentState = { necessary: 'granted', analytics: 'denied', marketing: 'denied', functional: 'denied', timestamp: Date.now(), version: CONSENT_VERSION }; this.updateConsent(newState); } /** * Update consent with specific preferences */ setPreferences(preferences: ConsentPreferences): void { const newState: ConsentState = { necessary: 'granted', analytics: preferences.analytics ? 'granted' : 'denied', marketing: preferences.marketing ? 'granted' : 'denied', functional: preferences.functional ? 'granted' : 'denied', timestamp: Date.now(), version: CONSENT_VERSION }; this.updateConsent(newState); } private updateConsent(state: ConsentState): void { this.consentState$.next(state); this.saveConsent(state); this.bannerVisible$.next(false); this.updateGoogleConsentMode(state); this.pushToDataLayer(state); } /** * Show the consent banner (for preference management) */ showBanner(): void { this.bannerVisible$.next(true); } /** * Hide the consent banner */ hideBanner(): void { this.bannerVisible$.next(false); } /** * Get current consent state synchronously */ getCurrentConsent(): ConsentState { return this.consentState$.getValue(); } /** * Check if a specific category is granted */ isGranted(category: ConsentCategory): boolean { return this.consentState$.getValue()[category] === 'granted'; } /** * Wait for consent to be resolved (not pending) */ waitForConsent(category: ConsentCategory): Observable { return this.consent$.pipe( map(state => state[category]), filter(status => status !== 'pending'), distinctUntilChanged() ); } private setDefaultGoogleConsentMode(): void { if (typeof window !== 'undefined' && (window as any).gtag) { (window as any).gtag('consent', 'default', { 'analytics_storage': 'denied', 'ad_storage': 'denied', 'ad_user_data': 'denied', 'ad_personalization': 'denied', 'functionality_storage': 'denied', 'personalization_storage': 'denied', 'security_storage': 'granted' }); } } private updateGoogleConsentMode(state: ConsentState): void { if (typeof window !== 'undefined' && (window as any).gtag) { (window as any).gtag('consent', 'update', { 'analytics_storage': state.analytics === 'granted' ? 'granted' : 'denied', 'ad_storage': state.marketing === 'granted' ? 'granted' : 'denied', 'ad_user_data': state.marketing === 'granted' ? 'granted' : 'denied', 'ad_personalization': state.marketing === 'granted' ? 'granted' : 'denied', 'functionality_storage': state.functional === 'granted' ? 'granted' : 'denied', 'personalization_storage': state.functional === 'granted' ? 'granted' : 'denied', 'security_storage': 'granted' }); } } private pushToDataLayer(state: ConsentState): void { if (typeof window !== 'undefined') { (window as any).dataLayer = (window as any).dataLayer || []; (window as any).dataLayer.push({ event: 'consent_update', consent_analytics: state.analytics, consent_marketing: state.marketing, consent_functional: state.functional, consent_timestamp: state.timestamp }); } } } ``` ### Creating the Consent Banner Component Now let's create a consent banner component that uses the service: ```typescript // consent-banner.component.ts import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { ConsentService, ConsentPreferences } from './consent.service'; import { trigger, state, style, transition, animate } from '@angular/animations'; @Component({ selector: 'app-consent-banner', standalone: true, imports: [CommonModule, FormsModule], animations: [ trigger('slideUp', [ state('void', style({ transform: 'translateY(100%)', opacity: 0 })), state('*', style({ transform: 'translateY(0)', opacity: 1 })), transition('void => *', animate('300ms ease-out')), transition('* => void', animate('200ms ease-in')) ]) ], template: ` `, styles: [` .consent-banner { position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15); z-index: 10000; padding: 24px; } .consent-content { max-width: 1200px; margin: 0 auto; position: relative; } .consent-header h2 { margin: 0 0 8px 0; font-size: 18px; color: #1a1a1a; } .consent-header p { margin: 0 0 16px 0; color: #666; font-size: 14px; line-height: 1.5; } .consent-details { margin-bottom: 16px; padding: 16px; background: #f8f9fa; border-radius: 8px; } .consent-category { margin-bottom: 12px; } .consent-category:last-child { margin-bottom: 0; } .consent-toggle { display: flex; align-items: flex-start; cursor: pointer; } .consent-toggle input { display: none; } .toggle-slider { width: 44px; height: 24px; background: #ccc; border-radius: 12px; position: relative; transition: background 0.2s; flex-shrink: 0; margin-right: 12px; } .toggle-slider::after { content: ''; position: absolute; width: 20px; height: 20px; background: white; border-radius: 50%; top: 2px; left: 2px; transition: transform 0.2s; } .toggle-slider.disabled { background: #4CAF50; opacity: 0.7; } .toggle-slider.disabled::after { transform: translateX(20px); } input:checked + .toggle-slider { background: #4CAF50; } input:checked + .toggle-slider::after { transform: translateX(20px); } .toggle-label { display: flex; flex-direction: column; } .toggle-label strong { color: #1a1a1a; font-size: 14px; } .toggle-label small { color: #666; font-size: 12px; margin-top: 2px; } .consent-actions { display: flex; gap: 12px; flex-wrap: wrap; } .btn { padding: 12px 24px; border: none; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .btn-primary { background: #4CAF50; color: white; } .btn-primary:hover { background: #45a049; } .btn-secondary { background: #f0f0f0; color: #333; } .btn-secondary:hover { background: #e0e0e0; } .consent-close { position: absolute; top: 0; right: 0; background: none; border: none; font-size: 24px; color: #999; cursor: pointer; padding: 4px 8px; } .consent-close:hover { color: #333; } @media (max-width: 768px) { .consent-banner { padding: 16px; } .consent-actions { flex-direction: column; } .btn { width: 100%; } } `] }) export class ConsentBannerComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); showBanner = false; showDetails = false; preferences: ConsentPreferences = { analytics: false, marketing: false, functional: false }; constructor(private consentService: ConsentService) {} ngOnInit(): void { this.consentService.showBanner$ .pipe(takeUntil(this.destroy$)) .subscribe(show => { this.showBanner = show; if (show) { // Reset to current preferences when showing const current = this.consentService.getCurrentConsent(); this.preferences = { analytics: current.analytics === 'granted', marketing: current.marketing === 'granted', functional: current.functional === 'granted' }; } }); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } toggleDetails(): void { this.showDetails = !this.showDetails; } onPreferenceChange(): void { // Could add analytics tracking here } acceptAll(): void { this.consentService.acceptAll(); } denyAll(): void { this.consentService.denyAll(); } savePreferences(): void { this.consentService.setPreferences(this.preferences); } } ``` ### Consent-Aware Route Guards One of the most powerful patterns in Angular is using route guards to control access based on consent. Here's how to create guards that wait for consent before loading certain routes: ```typescript // consent.guard.ts import { Injectable } from '@angular/core'; import { CanActivate, CanLoad, Route, UrlSegment, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, take, switchMap, filter } from 'rxjs/operators'; import { ConsentService, ConsentCategory } from './consent.service'; @Injectable({ providedIn: 'root' }) export class AnalyticsConsentGuard implements CanActivate, CanLoad { constructor( private consentService: ConsentService, private router: Router ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable { return this.checkConsent('analytics'); } canLoad(route: Route, segments: UrlSegment[]): Observable { return this.checkConsent('analytics'); } private checkConsent(category: ConsentCategory): Observable { // Wait for initialization return this.consentService.isInitialized$.pipe( filter(initialized => initialized), take(1), switchMap(() => { // Check if consent is already granted if (this.consentService.isGranted(category)) { return of(true); } // Wait for consent decision return this.consentService.waitForConsent(category).pipe( take(1), map(status => status === 'granted') ); }) ); } } // Generic consent guard factory export function createConsentGuard(category: ConsentCategory) { @Injectable({ providedIn: 'root' }) class ConsentGuard implements CanActivate, CanLoad { constructor( private consentService: ConsentService, private router: Router ) {} canActivate(): Observable { return this.consentService.isInitialized$.pipe( filter(init => init), take(1), switchMap(() => this.consentService.waitForConsent(category)), take(1), map(status => status === 'granted') ); } canLoad(): Observable { return this.canActivate(); } } return ConsentGuard; } ``` ### Lazy Loading with Consent Angular's lazy loading can be combined with consent to only load heavy analytics or marketing modules when consent is granted: ```typescript // app-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AnalyticsConsentGuard } from './consent.guard'; const routes: Routes = [ { path: '', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) }, { path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), // Only load the analytics module if analytics consent is granted canLoad: [AnalyticsConsentGuard] }, { path: 'personalized', loadChildren: () => import('./personalized/personalized.module').then(m => m.PersonalizedModule), data: { requiresConsent: 'functional' } } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule {} ``` ### Consent-Aware Analytics Service Here's a complete analytics service that only tracks when consent is granted: ```typescript // analytics.service.ts import { Injectable, OnDestroy } from '@angular/core'; import { Router, NavigationEnd } from '@angular/router'; import { Subject } from 'rxjs'; import { takeUntil, filter, withLatestFrom } from 'rxjs/operators'; import { ConsentService } from './consent.service'; declare global { interface Window { gtag: (...args: any[]) => void; dataLayer: any[]; } } interface AnalyticsEvent { action: string; category: string; label?: string; value?: number; nonInteraction?: boolean; } interface QueuedEvent { type: 'pageview' | 'event'; data: any; timestamp: number; } @Injectable({ providedIn: 'root' }) export class AnalyticsService implements OnDestroy { private destroy$ = new Subject(); private eventQueue: QueuedEvent[] = []; private isAnalyticsLoaded = false; private maxQueueAge = 30 * 60 * 1000; // 30 minutes constructor( private consentService: ConsentService, private router: Router ) { this.initializeAnalytics(); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } private initializeAnalytics(): void { // Subscribe to analytics consent this.consentService.analyticsGranted$ .pipe(takeUntil(this.destroy$)) .subscribe(granted => { if (granted && !this.isAnalyticsLoaded) { this.loadAnalyticsScripts(); this.processEventQueue(); } else if (!granted && this.isAnalyticsLoaded) { // Consent was revoked this.disableAnalytics(); } }); // Track page views on navigation this.router.events .pipe( filter(event => event instanceof NavigationEnd), withLatestFrom(this.consentService.analyticsGranted$), takeUntil(this.destroy$) ) .subscribe(([event, analyticsGranted]) => { const navEnd = event as NavigationEnd; this.trackPageView(navEnd.urlAfterRedirects, analyticsGranted); }); } private loadAnalyticsScripts(): void { if (this.isAnalyticsLoaded) return; // Load Google Analytics const script = document.createElement('script'); script.async = true; script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX'; document.head.appendChild(script); window.dataLayer = window.dataLayer || []; window.gtag = function() { window.dataLayer.push(arguments); }; window.gtag('js', new Date()); window.gtag('config', 'G-XXXXXXXXXX', { send_page_view: false // We'll send page views manually }); this.isAnalyticsLoaded = true; console.log('Analytics loaded with consent'); } private disableAnalytics(): void { // Disable tracking window['ga-disable-G-XXXXXXXXXX'] = true; this.isAnalyticsLoaded = false; console.log('Analytics disabled - consent revoked'); } private processEventQueue(): void { const now = Date.now(); // Filter out old events const validEvents = this.eventQueue.filter( event => now - event.timestamp < this.maxQueueAge ); // Process valid events validEvents.forEach(event => { if (event.type === 'pageview') { this.sendPageView(event.data.path); } else if (event.type === 'event') { this.sendEvent(event.data); } }); // Clear the queue this.eventQueue = []; } private trackPageView(path: string, analyticsGranted: boolean): void { if (analyticsGranted && this.isAnalyticsLoaded) { this.sendPageView(path); } else if (!analyticsGranted) { // Queue for later if consent might be given this.queueEvent('pageview', { path }); } } private sendPageView(path: string): void { if (window.gtag) { window.gtag('event', 'page_view', { page_path: path, page_title: document.title }); } } /** * Track a custom event */ trackEvent(event: AnalyticsEvent): void { if (this.consentService.isGranted('analytics') && this.isAnalyticsLoaded) { this.sendEvent(event); } else { this.queueEvent('event', event); } } private sendEvent(event: AnalyticsEvent): void { if (window.gtag) { window.gtag('event', event.action, { event_category: event.category, event_label: event.label, value: event.value, non_interaction: event.nonInteraction }); } } private queueEvent(type: 'pageview' | 'event', data: any): void { this.eventQueue.push({ type, data, timestamp: Date.now() }); // Limit queue size if (this.eventQueue.length > 100) { this.eventQueue = this.eventQueue.slice(-100); } } /** * Track timing events */ trackTiming(category: string, variable: string, value: number, label?: string): void { if (this.consentService.isGranted('analytics') && this.isAnalyticsLoaded && window.gtag) { window.gtag('event', 'timing_complete', { name: variable, value: value, event_category: category, event_label: label }); } } /** * Track exceptions */ trackException(description: string, fatal = false): void { if (this.consentService.isGranted('analytics') && this.isAnalyticsLoaded && window.gtag) { window.gtag('event', 'exception', { description: description, fatal: fatal }); } } } ``` ### Using APP_INITIALIZER for Consent To ensure consent is loaded before your application bootstraps: ```typescript // app.module.ts import { NgModule, APP_INITIALIZER } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ConsentService } from './consent.service'; import { ConsentBannerComponent } from './consent-banner.component'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; export function initializeConsent(consentService: ConsentService) { return () => new Promise((resolve) => { // Wait for consent service to initialize const subscription = consentService.isInitialized$.subscribe(initialized => { if (initialized) { subscription.unsubscribe(); resolve(); } }); }); } @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, BrowserAnimationsModule, AppRoutingModule, ConsentBannerComponent ], providers: [ { provide: APP_INITIALIZER, useFactory: initializeConsent, deps: [ConsentService], multi: true } ], bootstrap: [AppComponent] }) export class AppModule {} ``` ## Vue.js CMP Integration ### Setting Up Pinia Store for Consent Vue 3 with Pinia provides an excellent foundation for consent management. Let's create a comprehensive consent store: ```typescript // stores/consent.ts import { defineStore } from 'pinia'; import { ref, computed, watch } from 'vue'; export type ConsentCategory = 'necessary' | 'analytics' | 'marketing' | 'functional'; export type ConsentStatus = 'pending' | 'granted' | 'denied'; export interface ConsentState { necessary: ConsentStatus; analytics: ConsentStatus; marketing: ConsentStatus; functional: ConsentStatus; timestamp: number; version: string; } export interface ConsentPreferences { analytics: boolean; marketing: boolean; functional: boolean; } const CONSENT_STORAGE_KEY = 'olc_consent_state'; const CONSENT_VERSION = '2.0'; export const useConsentStore = defineStore('consent', () => { // State const consentState = ref({ necessary: 'granted', analytics: 'pending', marketing: 'pending', functional: 'pending', timestamp: 0, version: CONSENT_VERSION }); const initialized = ref(false); const bannerVisible = ref(false); // Getters const analyticsConsent = computed(() => consentState.value.analytics); const marketingConsent = computed(() => consentState.value.marketing); const functionalConsent = computed(() => consentState.value.functional); const analyticsGranted = computed(() => consentState.value.analytics === 'granted'); const marketingGranted = computed(() => consentState.value.marketing === 'granted'); const functionalGranted = computed(() => consentState.value.functional === 'granted'); const hasDecided = computed(() => consentState.value.analytics !== 'pending' || consentState.value.marketing !== 'pending' || consentState.value.functional !== 'pending' ); // Actions function loadStoredConsent(): ConsentState | null { try { const stored = localStorage.getItem(CONSENT_STORAGE_KEY); if (!stored) return null; const parsed = JSON.parse(stored) as ConsentState; if (!parsed.timestamp || !parsed.version) return null; if (parsed.version !== CONSENT_VERSION) return null; // Check expiration (1 year) const oneYearAgo = Date.now() - (365 * 24 * 60 * 60 * 1000); if (parsed.timestamp < oneYearAgo) return null; return parsed; } catch { return null; } } function saveConsent(state: ConsentState): void { try { localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(state)); } catch (e) { console.warn('Failed to save consent:', e); } } function initialize(): void { if (initialized.value) return; const stored = loadStoredConsent(); if (stored) { consentState.value = stored; bannerVisible.value = false; updateGoogleConsentMode(stored); } else { bannerVisible.value = true; setDefaultGoogleConsentMode(); } initialized.value = true; } function acceptAll(): void { const newState: ConsentState = { necessary: 'granted', analytics: 'granted', marketing: 'granted', functional: 'granted', timestamp: Date.now(), version: CONSENT_VERSION }; updateConsent(newState); } function denyAll(): void { const newState: ConsentState = { necessary: 'granted', analytics: 'denied', marketing: 'denied', functional: 'denied', timestamp: Date.now(), version: CONSENT_VERSION }; updateConsent(newState); } function setPreferences(preferences: ConsentPreferences): void { const newState: ConsentState = { necessary: 'granted', analytics: preferences.analytics ? 'granted' : 'denied', marketing: preferences.marketing ? 'granted' : 'denied', functional: preferences.functional ? 'granted' : 'denied', timestamp: Date.now(), version: CONSENT_VERSION }; updateConsent(newState); } function updateConsent(state: ConsentState): void { consentState.value = state; saveConsent(state); bannerVisible.value = false; updateGoogleConsentMode(state); pushToDataLayer(state); } function showBanner(): void { bannerVisible.value = true; } function hideBanner(): void { bannerVisible.value = false; } function isGranted(category: ConsentCategory): boolean { return consentState.value[category] === 'granted'; } // Google Consent Mode integration function setDefaultGoogleConsentMode(): void { if (typeof window !== 'undefined' && (window as any).gtag) { (window as any).gtag('consent', 'default', { 'analytics_storage': 'denied', 'ad_storage': 'denied', 'ad_user_data': 'denied', 'ad_personalization': 'denied', 'functionality_storage': 'denied', 'personalization_storage': 'denied', 'security_storage': 'granted' }); } } function updateGoogleConsentMode(state: ConsentState): void { if (typeof window !== 'undefined' && (window as any).gtag) { (window as any).gtag('consent', 'update', { 'analytics_storage': state.analytics === 'granted' ? 'granted' : 'denied', 'ad_storage': state.marketing === 'granted' ? 'granted' : 'denied', 'ad_user_data': state.marketing === 'granted' ? 'granted' : 'denied', 'ad_personalization': state.marketing === 'granted' ? 'granted' : 'denied', 'functionality_storage': state.functional === 'granted' ? 'granted' : 'denied', 'personalization_storage': state.functional === 'granted' ? 'granted' : 'denied', 'security_storage': 'granted' }); } } function pushToDataLayer(state: ConsentState): void { if (typeof window !== 'undefined') { (window as any).dataLayer = (window as any).dataLayer || []; (window as any).dataLayer.push({ event: 'consent_update', consent_analytics: state.analytics, consent_marketing: state.marketing, consent_functional: state.functional, consent_timestamp: state.timestamp }); } } return { // State consentState, initialized, bannerVisible, // Getters analyticsConsent, marketingConsent, functionalConsent, analyticsGranted, marketingGranted, functionalGranted, hasDecided, // Actions initialize, acceptAll, denyAll, setPreferences, showBanner, hideBanner, isGranted }; }); ``` ### Vue Composable for Consent Create a composable that makes consent management easy to use in any component: ```typescript // composables/useConsent.ts import { computed, watch, onMounted, onUnmounted, ref } from 'vue'; import { useConsentStore, ConsentCategory, ConsentStatus } from '@/stores/consent'; interface UseConsentOptions { category?: ConsentCategory; onGranted?: () => void; onDenied?: () => void; onChanged?: (status: ConsentStatus) => void; } export function useConsent(options: UseConsentOptions = {}) { const store = useConsentStore(); const isLoading = ref(true); // Initialize store on mount onMounted(() => { store.initialize(); isLoading.value = false; }); // Watch for consent changes if category specified if (options.category) { const stopWatch = watch( () => store.consentState[options.category!], (newStatus, oldStatus) => { if (newStatus === oldStatus) return; options.onChanged?.(newStatus); if (newStatus === 'granted') { options.onGranted?.(); } else if (newStatus === 'denied') { options.onDenied?.(); } }, { immediate: true } ); onUnmounted(() => { stopWatch(); }); } // Computed properties for the specified category const status = computed(() => options.category ? store.consentState[options.category] : null ); const isGranted = computed(() => options.category ? store.isGranted(options.category) : false ); const isPending = computed(() => options.category ? store.consentState[options.category] === 'pending' : false ); return { // Store state consentState: computed(() => store.consentState), initialized: computed(() => store.initialized), bannerVisible: computed(() => store.bannerVisible), isLoading, // Category-specific status, isGranted, isPending, // All category states analyticsGranted: computed(() => store.analyticsGranted), marketingGranted: computed(() => store.marketingGranted), functionalGranted: computed(() => store.functionalGranted), // Actions acceptAll: store.acceptAll, denyAll: store.denyAll, setPreferences: store.setPreferences, showBanner: store.showBanner, hideBanner: store.hideBanner, checkConsent: store.isGranted }; } // Specialized composable for analytics consent export function useAnalyticsConsent() { return useConsent({ category: 'analytics' }); } // Specialized composable for marketing consent export function useMarketingConsent() { return useConsent({ category: 'marketing' }); } // Specialized composable for functional consent export function useFunctionalConsent() { return useConsent({ category: 'functional' }); } ``` ### Consent Banner Component for Vue ```vue ``` ### Vue Router Navigation Guards Implement consent-aware navigation guards for Vue Router: ```typescript // router/guards.ts import { RouteLocationNormalized, NavigationGuardNext } from 'vue-router'; import { useConsentStore, ConsentCategory } from '@/stores/consent'; export function createConsentGuard(requiredCategory: ConsentCategory) { return async ( to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext ) => { const store = useConsentStore(); // Wait for initialization if (!store.initialized) { store.initialize(); } // Check consent if (store.isGranted(requiredCategory)) { next(); } else { // Option 1: Redirect to a fallback page // next({ name: 'consent-required', query: { redirect: to.fullPath } }); // Option 2: Show consent banner and wait store.showBanner(); // Watch for consent change const unwatch = store.$subscribe((mutation, state) => { if (state.consentState[requiredCategory] === 'granted') { unwatch(); next(); } else if (state.consentState[requiredCategory] === 'denied') { unwatch(); next(false); } }); } }; } // Usage in router // router/index.ts import { createRouter, createWebHistory } from 'vue-router'; import { createConsentGuard } from './guards'; const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: () => import('@/views/Home.vue') }, { path: '/analytics-dashboard', component: () => import('@/views/AnalyticsDashboard.vue'), beforeEnter: createConsentGuard('analytics') }, { path: '/personalized-recommendations', component: () => import('@/views/Recommendations.vue'), beforeEnter: createConsentGuard('functional') } ] }); export default router; ``` ### Consent-Aware Analytics Composable ```typescript // composables/useAnalytics.ts import { ref, watch, onMounted, onUnmounted } from 'vue'; import { useRouter } from 'vue-router'; import { useAnalyticsConsent } from './useConsent'; interface AnalyticsEvent { action: string; category: string; label?: string; value?: number; } interface QueuedEvent { type: 'pageview' | 'event'; data: any; timestamp: number; } declare global { interface Window { gtag: (...args: any[]) => void; dataLayer: any[]; } } export function useAnalytics() { const router = useRouter(); const { isGranted, status } = useAnalyticsConsent(); const isLoaded = ref(false); const eventQueue = ref([]); const MAX_QUEUE_AGE = 30 * 60 * 1000; // 30 minutes // Load analytics when consent is granted watch(isGranted, (granted) => { if (granted && !isLoaded.value) { loadAnalytics(); processQueue(); } else if (!granted && isLoaded.value) { disableAnalytics(); } }, { immediate: true }); // Track page views on route change onMounted(() => { router.afterEach((to) => { trackPageView(to.fullPath); }); }); function loadAnalytics() { if (isLoaded.value) return; const script = document.createElement('script'); script.async = true; script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX'; document.head.appendChild(script); window.dataLayer = window.dataLayer || []; window.gtag = function() { window.dataLayer.push(arguments); }; window.gtag('js', new Date()); window.gtag('config', 'G-XXXXXXXXXX', { send_page_view: false }); isLoaded.value = true; } function disableAnalytics() { window['ga-disable-G-XXXXXXXXXX'] = true; isLoaded.value = false; } function processQueue() { const now = Date.now(); const validEvents = eventQueue.value.filter( e => now - e.timestamp < MAX_QUEUE_AGE ); validEvents.forEach(event => { if (event.type === 'pageview') { sendPageView(event.data.path); } else { sendEvent(event.data); } }); eventQueue.value = []; } function queueEvent(type: 'pageview' | 'event', data: any) { eventQueue.value.push({ type, data, timestamp: Date.now() }); // Limit queue size if (eventQueue.value.length > 100) { eventQueue.value = eventQueue.value.slice(-100); } } function trackPageView(path: string) { if (isGranted.value && isLoaded.value) { sendPageView(path); } else if (status.value === 'pending') { queueEvent('pageview', { path }); } } function sendPageView(path: string) { if (window.gtag) { window.gtag('event', 'page_view', { page_path: path, page_title: document.title }); } } function trackEvent(event: AnalyticsEvent) { if (isGranted.value && isLoaded.value) { sendEvent(event); } else if (status.value === 'pending') { queueEvent('event', event); } } function sendEvent(event: AnalyticsEvent) { if (window.gtag) { window.gtag('event', event.action, { event_category: event.category, event_label: event.label, value: event.value }); } } return { isLoaded, trackPageView, trackEvent }; } ``` ## Framework-Agnostic Best Practices ### Script Loading Strategies Both Angular and Vue applications need a consistent approach to loading third-party scripts based on consent: ```typescript // utils/scriptLoader.ts interface ScriptConfig { src: string; id: string; async?: boolean; defer?: boolean; attributes?: Record; onLoad?: () => void; onError?: (error: Error) => void; } class ScriptLoader { private loadedScripts = new Set(); private loadingScripts = new Map>(); async load(config: ScriptConfig): Promise { // Already loaded if (this.loadedScripts.has(config.id)) { return Promise.resolve(); } // Currently loading if (this.loadingScripts.has(config.id)) { return this.loadingScripts.get(config.id)!; } const loadPromise = new Promise((resolve, reject) => { const script = document.createElement('script'); script.id = config.id; script.src = config.src; script.async = config.async ?? true; script.defer = config.defer ?? false; if (config.attributes) { Object.entries(config.attributes).forEach(([key, value]) => { script.setAttribute(key, value); }); } script.onload = () => { this.loadedScripts.add(config.id); this.loadingScripts.delete(config.id); config.onLoad?.(); resolve(); }; script.onerror = () => { this.loadingScripts.delete(config.id); const error = new Error(`Failed to load script: ${config.src}`); config.onError?.(error); reject(error); }; document.head.appendChild(script); }); this.loadingScripts.set(config.id, loadPromise); return loadPromise; } unload(id: string): void { const script = document.getElementById(id); if (script) { script.remove(); this.loadedScripts.delete(id); } } isLoaded(id: string): boolean { return this.loadedScripts.has(id); } } export const scriptLoader = new ScriptLoader(); ``` ### Consent-Aware Third-Party Integration Here's a pattern for integrating any third-party service with consent awareness: ```typescript // services/thirdPartyManager.ts import { scriptLoader } from '@/utils/scriptLoader'; type ConsentChecker = () => boolean; type ConsentWatcher = (callback: () => void) => () => void; interface ThirdPartyService { id: string; name: string; consentCategory: 'analytics' | 'marketing' | 'functional'; scripts: Array<{ src: string; attributes?: Record; }>; initialize: () => void; cleanup?: () => void; } class ThirdPartyManager { private services: Map = new Map(); private activeServices: Set = new Set(); private consentChecker: ConsentChecker; private consentWatcher: ConsentWatcher; private unwatchFns: Map void> = new Map(); constructor( consentChecker: ConsentChecker, consentWatcher: ConsentWatcher ) { this.consentChecker = consentChecker; this.consentWatcher = consentWatcher; } register(service: ThirdPartyService): void { this.services.set(service.id, service); // Watch for consent changes const unwatch = this.consentWatcher(() => { this.checkAndUpdateService(service); }); this.unwatchFns.set(service.id, unwatch); // Initial check this.checkAndUpdateService(service); } private async checkAndUpdateService(service: ThirdPartyService): Promise { const hasConsent = this.consentChecker(); const isActive = this.activeServices.has(service.id); if (hasConsent && !isActive) { await this.activateService(service); } else if (!hasConsent && isActive) { this.deactivateService(service); } } private async activateService(service: ThirdPartyService): Promise { try { // Load all scripts for (const script of service.scripts) { await scriptLoader.load({ id: `${service.id}-${script.src.split('/').pop()}`, src: script.src, attributes: script.attributes }); } // Run initialization service.initialize(); this.activeServices.add(service.id); console.log(`${service.name} activated with consent`); } catch (error) { console.error(`Failed to activate ${service.name}:`, error); } } private deactivateService(service: ThirdPartyService): void { // Run cleanup if provided service.cleanup?.(); // Remove scripts for (const script of service.scripts) { scriptLoader.unload(`${service.id}-${script.src.split('/').pop()}`); } this.activeServices.delete(service.id); console.log(`${service.name} deactivated - consent revoked`); } unregister(serviceId: string): void { const service = this.services.get(serviceId); if (service) { this.deactivateService(service); this.unwatchFns.get(serviceId)?.(); this.unwatchFns.delete(serviceId); this.services.delete(serviceId); } } getActiveServices(): string[] { return Array.from(this.activeServices); } } export { ThirdPartyManager, ThirdPartyService }; ``` ## Testing Consent Integration ### Unit Testing Angular Consent Service ```typescript // consent.service.spec.ts import { TestBed } from '@angular/core/testing'; import { ConsentService } from './consent.service'; describe('ConsentService', () => { let service: ConsentService; beforeEach(() => { // Clear localStorage before each test localStorage.clear(); TestBed.configureTestingModule({}); service = TestBed.inject(ConsentService); }); afterEach(() => { localStorage.clear(); }); it('should be created', () => { expect(service).toBeTruthy(); }); it('should show banner when no stored consent', (done) => { service.showBanner$.subscribe(show => { expect(show).toBe(true); done(); }); }); it('should accept all consent categories', (done) => { service.acceptAll(); service.consent$.subscribe(state => { expect(state.analytics).toBe('granted'); expect(state.marketing).toBe('granted'); expect(state.functional).toBe('granted'); done(); }); }); it('should deny all optional consent categories', (done) => { service.denyAll(); service.consent$.subscribe(state => { expect(state.analytics).toBe('denied'); expect(state.marketing).toBe('denied'); expect(state.functional).toBe('denied'); expect(state.necessary).toBe('granted'); // Always granted done(); }); }); it('should persist consent to localStorage', () => { service.acceptAll(); const stored = localStorage.getItem('olc_consent_state'); expect(stored).toBeTruthy(); const parsed = JSON.parse(stored!); expect(parsed.analytics).toBe('granted'); }); it('should restore consent from localStorage', () => { // Store consent const mockConsent = { necessary: 'granted', analytics: 'granted', marketing: 'denied', functional: 'granted', timestamp: Date.now(), version: '2.0' }; localStorage.setItem('olc_consent_state', JSON.stringify(mockConsent)); // Create new service instance const newService = new ConsentService(); expect(newService.getCurrentConsent().analytics).toBe('granted'); expect(newService.getCurrentConsent().marketing).toBe('denied'); }); it('should emit analyticsGranted$ correctly', (done) => { let emissions: boolean[] = []; service.analyticsGranted$.subscribe(granted => { emissions.push(granted); if (emissions.length === 2) { expect(emissions[0]).toBe(false); // Initial pending state expect(emissions[1]).toBe(true); // After acceptAll done(); } }); service.acceptAll(); }); }); ``` ### Testing Vue Consent Store ```typescript // stores/consent.spec.ts import { setActivePinia, createPinia } from 'pinia'; import { useConsentStore } from './consent'; describe('Consent Store', () => { beforeEach(() => { setActivePinia(createPinia()); localStorage.clear(); }); afterEach(() => { localStorage.clear(); }); it('initializes with pending state', () => { const store = useConsentStore(); store.initialize(); expect(store.consentState.analytics).toBe('pending'); expect(store.bannerVisible).toBe(true); }); it('accepts all consent', () => { const store = useConsentStore(); store.initialize(); store.acceptAll(); expect(store.analyticsGranted).toBe(true); expect(store.marketingGranted).toBe(true); expect(store.functionalGranted).toBe(true); expect(store.bannerVisible).toBe(false); }); it('denies all optional consent', () => { const store = useConsentStore(); store.initialize(); store.denyAll(); expect(store.analyticsGranted).toBe(false); expect(store.marketingGranted).toBe(false); expect(store.functionalGranted).toBe(false); }); it('persists consent to localStorage', () => { const store = useConsentStore(); store.initialize(); store.acceptAll(); const stored = localStorage.getItem('olc_consent_state'); expect(stored).toBeTruthy(); const parsed = JSON.parse(stored!); expect(parsed.analytics).toBe('granted'); }); it('restores consent from localStorage', () => { const mockConsent = { necessary: 'granted', analytics: 'granted', marketing: 'denied', functional: 'granted', timestamp: Date.now(), version: '2.0' }; localStorage.setItem('olc_consent_state', JSON.stringify(mockConsent)); const store = useConsentStore(); store.initialize(); expect(store.analyticsGranted).toBe(true); expect(store.marketingGranted).toBe(false); expect(store.bannerVisible).toBe(false); }); it('shows banner when consent version changes', () => { const oldConsent = { necessary: 'granted', analytics: 'granted', marketing: 'granted', functional: 'granted', timestamp: Date.now(), version: '1.0' // Old version }; localStorage.setItem('olc_consent_state', JSON.stringify(oldConsent)); const store = useConsentStore(); store.initialize(); expect(store.bannerVisible).toBe(true); }); }); ``` ### E2E Testing with Playwright ```typescript // e2e/consent.spec.ts import { test, expect } from '@playwright/test'; test.describe('Consent Banner', () => { test.beforeEach(async ({ page, context }) => { // Clear cookies and localStorage await context.clearCookies(); await page.goto('/'); await page.evaluate(() => localStorage.clear()); await page.reload(); }); test('shows consent banner on first visit', async ({ page }) => { await page.goto('/'); const banner = page.locator('[role="dialog"]'); await expect(banner).toBeVisible(); await expect(banner).toContainText('We value your privacy'); }); test('accepts all cookies', async ({ page }) => { await page.goto('/'); await page.click('text=Accept All'); // Banner should hide const banner = page.locator('[role="dialog"]'); await expect(banner).not.toBeVisible(); // Check localStorage const consent = await page.evaluate(() => JSON.parse(localStorage.getItem('olc_consent_state') || '{}') ); expect(consent.analytics).toBe('granted'); expect(consent.marketing).toBe('granted'); }); test('denies all cookies', async ({ page }) => { await page.goto('/'); await page.click('text=Deny All'); const consent = await page.evaluate(() => JSON.parse(localStorage.getItem('olc_consent_state') || '{}') ); expect(consent.analytics).toBe('denied'); expect(consent.marketing).toBe('denied'); }); test('customizes cookie preferences', async ({ page }) => { await page.goto('/'); await page.click('text=Customize'); // Toggle analytics on, keep marketing off await page.locator('text=Analytics').locator('..').locator('input').check(); await page.click('text=Save Preferences'); const consent = await page.evaluate(() => JSON.parse(localStorage.getItem('olc_consent_state') || '{}') ); expect(consent.analytics).toBe('granted'); expect(consent.marketing).toBe('denied'); }); test('remembers consent on subsequent visits', async ({ page }) => { await page.goto('/'); await page.click('text=Accept All'); // Navigate away and back await page.goto('/about'); await page.goto('/'); // Banner should not appear const banner = page.locator('[role="dialog"]'); await expect(banner).not.toBeVisible(); }); test('analytics script loads only after consent', async ({ page }) => { await page.goto('/'); // Check that GA is not loaded let gaLoaded = await page.evaluate(() => !!document.querySelector('script[src*="googletagmanager"]') ); expect(gaLoaded).toBe(false); // Accept cookies await page.click('text=Accept All'); // Wait for script to load await page.waitForTimeout(1000); gaLoaded = await page.evaluate(() => !!document.querySelector('script[src*="googletagmanager"]') ); expect(gaLoaded).toBe(true); }); }); ``` ## Common Mistakes to Avoid ### 1. Race Conditions with Script Loading **Wrong:** ```typescript // Script might fire before consent is checked ngOnInit() { this.loadAnalytics(); this.consentService.consent$.subscribe(consent => { // Too late! }); } ``` **Right:** ```typescript ngOnInit() { this.consentService.analyticsGranted$.pipe( filter(granted => granted), take(1) ).subscribe(() => { this.loadAnalytics(); }); } ``` ### 2. Memory Leaks from Subscriptions **Wrong:** ```typescript // No cleanup - subscription leaks export class MyComponent { ngOnInit() { this.consentService.consent$.subscribe(consent => { // This subscription never gets cleaned up }); } } ``` **Right:** ```typescript export class MyComponent implements OnDestroy { private destroy$ = new Subject(); ngOnInit() { this.consentService.consent$ .pipe(takeUntil(this.destroy$)) .subscribe(consent => { // Subscription is properly managed }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } } ``` ### 3. Not Handling SSR **Wrong:** ```typescript // Will crash during SSR constructor() { const stored = localStorage.getItem('consent'); } ``` **Right:** ```typescript constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const stored = localStorage.getItem('consent'); } } ``` ### 4. Blocking the Main Thread **Wrong:** ```typescript // Synchronous check blocks rendering if (this.checkConsentSynchronously()) { this.loadAllScripts(); } ``` **Right:** ```typescript // Async check doesn't block this.consentService.isInitialized$.pipe( filter(init => init), take(1), switchMap(() => this.consentService.analyticsGranted$) ).subscribe(granted => { if (granted) this.loadAnalytics(); }); ``` ## Putting It All Together Integrating consent management into Angular and Vue.js applications requires thinking reactively. The frameworks' strengths—dependency injection in Angular, the Composition API in Vue—make it possible to build consent systems that feel native rather than bolted-on. The key principles remain constant regardless of framework: 1. **State First, Scripts Second**: Never load third-party scripts until consent is confirmed 2. **Reactive Updates**: When consent changes, all dependent systems should react automatically 3. **Clean Subscriptions**: Prevent memory leaks by properly managing subscriptions and watchers 4. **SSR Awareness**: Handle server-side rendering gracefully 5. **Type Safety**: Use TypeScript to catch consent-related bugs at compile time 6. **Testability**: Build consent into your testing strategy from day one The code examples in this guide are production-ready. You can adapt them to your specific needs, whether you're building a simple blog or a complex enterprise application. The patterns scale because they're built on the same reactive foundations that power Angular and Vue themselves. Remember: good consent management isn't just about compliance—it's about building trust with your users. When users see that your application respects their privacy preferences instantly and consistently, they're more likely to engage with your content and, ultimately, more likely to grant the consent you need for analytics and personalization. Start with the consent service, build your banner, and work outward from there. Every component that touches user data should check consent first. It's more work upfront, but it pays dividends in user trust and regulatory compliance.
D

David Kim, WordPress Developer

Schrijver bij GetCookies, gespecialiseerd in privacy-compliance, toestemmingsbeheer en optimalisatie van digitale marketing.

Klaar om cookietoestemming te vereenvoudigen?

GetCookies maakt AVG, CCPA en wereldwijde privacy-compliance moeiteloos. Begin vandaag.