1/** 2 * webflow-privacy-banner.js 3 * Hosted here to provide GDPR-compliant privacy consent management for 4 * claude.com pages published via Webflow reverse proxy. 5 * 6 * Contains: 7 * - Cookie consent banner UI and logic 8 * - Country detection for GDPR compliance 9 * - Injection of the first-party analytics loader (webflow-antalytics.js, 10 * which loads the SDK only under analytics consent) 11 * - GTM consent mode integration 12 * - Webflow Optimize consent handling 13 * 14 * Dependencies: External consent banner HTML 15 */ 16 17/* Privacy Choices Consent Banner */ 18 19;(function () { 20 'use strict' 21 22 const antalyticsLoader = document.createElement('script') // nosemgrep: create-script-element 23 antalyticsLoader.async = true 24 antalyticsLoader.src = 'https://claude.com/shared/webflow-antalytics.js' 25 document.head.appendChild(antalyticsLoader) 26 27 // Constants 28 const CONSENT_PREFERENCES_COOKIE_KEY = 'anthropic-consent-preferences' 29 30 /** 31 * Countries and regions that require explicit user consent before setting 32 * non-essential cookies (analytics, marketing, etc.) under privacy regulations. 33 * 34 * This list includes: 35 * - EU 27 member states (GDPR) and the non-EU EEA states 36 * - EU member state overseas territories 37 * - United Kingdom and territories (UK GDPR) 38 * - Canada (PIPEDA) 39 * - Brazil (LGPD) 40 * - India (DPDPA) 41 * - Switzerland (nFADP) 42 * 43 * Mirrors frontend/src/lib/consentCountries.ts (a test checks the two 44 * match). The website repo's copy of this banner (anthropic.com) and the 45 * apps repo keep their own lists. 46 */ 47 const EXPLICIT_CONSENT_COUNTRIES = new Set([ 48 // EU 27 Member States (GDPR) 49 'AT', 50 'BE', 51 'BG', 52 'HR', 53 'CY', 54 'CZ', 55 'DK', 56 'EE', 57 'FI', 58 'FR', 59 'DE', 60 'GR', 61 'HU', 62 'IE', 63 'IT', 64 'LV', 65 'LT', 66 'LU', 67 'MT', 68 'NL', 69 'PL', 70 'PT', 71 'RO', 72 'SK', 73 'SI', 74 'ES', 75 'SE', 76 // EEA (non-EU), GDPR applies via the EEA Agreement 77 'IS', 78 'LI', 79 'NO', 80 // French overseas territories 81 'RE', 82 'GP', 83 'MQ', 84 'GF', 85 'YT', 86 'BL', 87 'MF', 88 'PM', 89 'WF', 90 'PF', 91 'NC', 92 // Dutch overseas territories 93 'AW', 94 'CW', 95 'SX', 96 // Danish territories 97 'FO', 98 'GL', 99 // Finnish territory 100 'AX', 101 // United Kingdom (UK GDPR) 102 'GB', 103 'UK', 104 // UK overseas territories and Crown Dependencies 105 'AI', 106 'BM', 107 'IO', 108 'VG', 109 'KY', 110 'FK', 111 'GI', 112 'MS', 113 'PN', 114 'SH', 115 'TC', 116 'GG', 117 'JE', 118 'IM', 119 // Other regions with privacy regulations 120 'CA', // Canada (PIPEDA) 121 'BR', // Brazil (LGPD) 122 'IN', // India (DPDPA) 123 'CH', // Switzerland (nFADP) 124 ]) 125 126 /** 127 * Configuration for cookies to delete when users opt out of tracking. 128 * Based on: https://privacy.claude.com/en/articles/10023541-what-cookies-does-anthropic-use 129 */ 130 const COOKIES_PER_CONSENT_CATEGORY = { 131 analytics: [ 132 // Segment 133 'ajs_anonymous_id', 134 'ajs_user_id', 135 'ajs_group_id', 136 'analytics_session_id', 137 // Google Analytics (first-party) 138 '_ga', 139 '_gid', 140 '_gat', 141 // Legacy Google Analytics 142 '__utma', 143 '__utmb', 144 '__utmc', 145 '__utmt', 146 '__utmz', 147 '__utmv', 148 // Google Optimize 149 '_gaexp', 150 '_gaexp_rc', 151 '_opt_expid', 152 // Other GA first-party cookies 153 'AMP_TOKEN', 154 'FPID', 155 'FPLC', 156 'TESTCOOKIESENABLED', 157 // LinkedIn 158 'li_giant', 159 'ln_or', 160 // Oribi 161 'oribi_cookie_test', 162 'oribili_user_guid', 163 ], 164 marketing: [ 165 // Facebook (first-party) 166 '_fbc', 167 '_fbp', 168 // Google Ads & Conversion Tracking (first-party) 169 '__gads', 170 '__gpi', 171 '__gpi_optout', 172 '__gsas', 173 '_gcl_aw', 174 '_gcl_dc', 175 '_gcl_au', 176 '_gcl_gb', 177 '_gcl_gf', 178 '_gcl_ha', 179 '_gcl_gs', 180 '_gcl_ag', 181 'GCL_AW_P', 182 'GED_PLAYLIST_ACTIVITY', 183 'ACLK_DATA', 184 'FLC', 185 // Google Ads first-party cookies 186 '_opt_awcid', 187 '_opt_awmid', 188 '_opt_awgid', 189 '_opt_awkid', 190 '_opt_utmc', 191 'FPAU', 192 'FPGCLDC', 193 'FPGCLAW', 194 'FPGCLGB', 195 'FPGSID', 196 'FCCDCF', 197 'FCNEC', 198 // LinkedIn (first-party) 199 'li_fat_id', 200 'ar_debug', 201 // TikTok (first-party) 202 '_ttclid', 203 // Reddit (first-party) 204 '_rdt_uuid', 205 '_rdt_cid', 206 ], 207 } 208 209 /** 210 * Cookie name patterns that should be deleted (regex-based matching). 211 */ 212 const COOKIE_PATTERNS_TO_DELETE = { 213 analytics: [/^_gat_gtag_UA_.*$/, /^_ga_.*$/, /^_dc_gtm_.*$/], 214 marketing: [/^_gac_.*$/, /^_gac_gb_.*$/], 215 } 216 217 /**
218 * Gets the appropriate domain for cookies based on hostname. 219 * Returns a domain string starting with '.' for cross-subdomain cookies, 220 * or undefined for unknown/local domains (which scopes cookie to current host only). 221 */ 222 function getDomainFromHost(hostname) { 223 if (hostname === 'claude.com' || hostname.endsWith('.claude.com')) { 224 return '.claude.com' 225 } 226 // For all other domains (including localhost, staging, etc.), return undefined 227 // This scopes cookies to the current host only 228 return undefined 229 } 230 231 // Cookie utilities 232 const cookieUtils = { 233 get(name) { 234 const cookies = document.cookie.split(';') 235 for (let i = 0; i < cookies.length; i++) { 236 const cookie = cookies[i].trim() 237 const [key, value] = cookie.split('=') 238 if (name === key) { 239 return decodeURIComponent(value) 240 } 241 } 242 return undefined 243 }, 244 245 set(name, value) { 246 const hostname = window.location.hostname 247 const domain = getDomainFromHost(hostname) 248 const path = '/' 249 250 const cookieParts = [ 251 `${name}=${encodeURIComponent(value)}`, 252 `max-age=${60 * 60 * 24 * 365}`, // 1 year in seconds 253 `path=${path}`, 254 'samesite=lax', 255 'secure', 256 ] 257 258 if (domain) { 259 cookieParts.push(`domain=${domain}`) 260 } 261 262 document.cookie = cookieParts.join('; ') 263 }, 264 265 has(name) { 266 return document.cookie.split(';').some((cookie) => cookie.trim().startsWith(`${name}=`)) 267 }, 268 269 delete(name) { 270 const hostname = window.location.hostname 271 const domain = getDomainFromHost(hostname) 272 const path = '/' 273 274 // Delete cookie by setting it with an expired date 275 const cookieParts = [`${name}=`, 'expires=Thu, 01 Jan 1970 00:00:00 GMT', `path=${path}`] 276 277 if (domain) { 278 cookieParts.push(`domain=${domain}`) 279 } 280 281 document.cookie = cookieParts.join('; ') 282 }, 283 } 284 285 /** 286 * Helper function to get all cookie names from document.cookie 287 */ 288 function getAllCookieNames() { 289 return document.cookie.split(';
289').map((cookie) => cookie.split('=')[0].trim()) 290 } 291 292 /** 293 * Helper function to check if a cookie name matches a pattern 294 * Supports both exact string matches and RegExp patterns 295 */ 296 function matchesCookiePattern(cookieName, pattern) { 297 if (pattern instanceof RegExp) { 298 return pattern.test(cookieName) 299 } 300 return cookieName === pattern 301 } 302 303 /** 304 * Deletes cookies for a specific consent category when user opts out. 305 * Automatically determines the appropriate domain from the current hostname. 306 */ 307 function deleteCookiesForCategory(category) { 308 const allCookieNames = getAllCookieNames() 309 310 // Delete named cookies 311 COOKIES_PER_CONSENT_CATEGORY[category].forEach((cookieName) => { 312 cookieUtils.delete(cookieName) 313 }) 314 315 // Delete pattern-based cookies (regex) 316 const patterns = COOKIE_PATTERNS_TO_DELETE[category] || [] 317 patterns.forEach((pattern) => { 318 allCookieNames.forEach((cookieName) => { 319 if (matchesCookiePattern(cookieName, pattern)) { 320 cookieUtils.delete(cookieName) 321 } 322 }) 323 }) 324 } 325 326 /** 327 * Checks if user is in a country requiring explicit consent. 328 * Uses server-side API first, then falls back to browser language detection. 329 */ 330 async function inExplicitConsentRequiredCountry() { 331 try { 332 // Try server-side country detection first 333 const response = await fetch('https://www.anthropic.com/api/country') 334 const data = await response.json() 335 336 if (data.country && typeof data.country === 'string') { 337 return EXPLICIT_CONSENT_COUNTRIES.has(data.country) 338 } 339 340 // Fallback to browser language detection 341 return fallbackLanguageDetection() 342 } catch (e) { 343 // Fallback to browser language detection on error 344 return fallbackLanguageDetection() 345 } 346 } 347 348 /** 349 * Fallback method using browser language to detect country. 350 */ 351 function fallbackLanguageDetection() { 352 try { 353 const browserLanguage = navigator.languages?.[0] || navigator.language 354 const browserLocale = browserLanguage.split('-')[1] 355 return EXPLICIT_CONSENT_COUNTRIES.has(browserLocale) 356 } catch (e) { 357 return false 358 } 359 } 360 361 /** 362 * Checks if Global Privacy Control (GPC) is enabled. 363 * GPC is a browser signal that indicates the user doesn't want their data sold or shared. 364 * Reference: https://globalprivacycontrol.org/ 365 */ 366 function isGlobalPrivacyControlEnabled() { 367 return navigator.globalPrivacyControl === true 368 } 369 370 /** 371 * Gets initial consent preferences from cookie or defaults based on country and GPC. 372 */ 373 async function getInitialConsentPreferences() { 374 // GPC takes precedence - if enabled, deny all tracking regardless of other settings 375 if (isGlobalPrivacyControlEnabled()) { 376 // Delete any existing tracking cookies when GPC is enabled 377 // This ensures cookies from previous sessions are removed when user enables GPC 378 deleteCookiesForCategory('analytics') 379 deleteCookiesForCategory('marketing') 380 return {analytics: false, marketing: false} 381 } 382 383 const consentPreferencesCookie = cookieUtils.get(CONSENT_PREFERENCES_COOKIE_KEY) 384 if (consentPreferencesCookie) { 385 try { 386 return JSON.parse(consentPreferencesCookie) 387 } catch { 388 // Fall through to default logic 389 } 390 } 391 392 // If no cookie exists, determine defaults based on country 393 const requiresExplicitConsent = await inExplicitConsentRequiredCountry() 394 return { 395 analytics: !requiresExplicitConsent, 396 marketing: !requiresExplicitConsent, 397 } 398 } 399 400 /** 401 * Updates Google Tag Manager consent preferences. 402 * Note: GTM is only loaded when marketing consent is granted, so this function 403 * primarily handles consent updates for users who change preferences mid-session. 404 * If GTM isn't loaded yet but marketing consent is now granted, it will load on 405 * the next page navigation when the consent cookie is read. 406 */ 407 function updateGTMConsentPreferences(preferences) { 408 if (typeof window.gtag !== 'function') return 409 410 window.gtag('consent', 'update', { 411 ad_personalization: preferences.marketing ? 'granted' : 'denied', 412 ad_user_data: preferences.marketing ? 'granted' : 'denied', 413 ad_storage: preferences.marketing ? 'granted' : 'denied', 414 analytics_storage: preferences.analytics ? 'granted' : 'denied', 415 functionality_storage: 'granted', 416 personalization_storage: 'granted', 417 security_storage: 'granted', 418 }) 419 } 420 421 /** 422 * Updates Webflow Optimize tracking based on analytics consent. 423 * Webflow Optimize respects analytics consent since A/B testing is analytics-related. 424 */ 425 function updateWebflowOptimizeConsent(preferences) { 426 if (typeof window.wf === 'undefined') return 427 428 // Wrap in wf.ready() to ensure tracking methods are available 429 if (typeof window.wf.ready === 'function') { 430 window.wf.ready(() => { 431 // Allow tracking if analytics is consented, deny otherwise 432 if (preferences.analytics) { 433 window.wf.allowUserTracking() 434 } else { 435 window.wf.denyUserTracking() 436 } 437 }) 438 } 439 } 440 441 /** 442 * Saves consent preferences and handles cookie deletion for rejected categories. 443 * Scripts will load/unload on next page navigation based on saved consent. 444 */ 445 function saveConsentPreferences(preferences) { 446 // Save preferences cookie 447 cookieUtils.set(CONSENT_PREFERENCES_COOKIE_KEY, JSON.stringify(preferences)) 448 449 // Keep the published verdict and the antalytics loader in sync 450 window.__anthropicEffectiveConsent = {...preferences} 451 window.dispatchEvent(new CustomEvent('anthropic-consent-updated')) 452 453 // Delete cookies for rejected categories
454 Object.keys(preferences).forEach((category) => { 455 if (!preferences[category] && COOKIES_PER_CONSENT_CATEGORY[category]) { 456 deleteCookiesForCategory(category) 457 } 458 }) 459 460 // Update GTM consent state 461 updateGTMConsentPreferences(preferences) 462 463 // Update Webflow Optimize consent state 464 updateWebflowOptimizeConsent(preferences) 465 466 // Hide banner 467 hideBanner() 468 } 469 470 // UI Functions 471 function showBanner() { 472 const dialog = document.getElementById('consent-container') 473 if (!dialog) return 474 475 dialog.show() 476 requestAnimationFrame(() => { 477 dialog.classList.add('show') 478 }) 479 480 document.getElementById('simple-options').style.display = 'grid' 481 document.getElementById('detailed-options').style.display = 'none' 482 document.getElementById('consent-description').innerHTML = 483 'We use cookies to deliver and improve our services, analyze site usage, and if you agree, to customize or personalize your experience and market our services to you. You can read our Cookie Policy <a href="https://www.anthropic.com/legal/cookies" style="color: #a1a0a0; text-decoration: underline;">here</a>.' 484 } 485 486 function hideBanner() { 487 const dialog = document.getElementById('consent-container') 488 if (!dialog) return 489 490 dialog.classList.remove('show') 491 setTimeout(() => dialog.close(), 300) 492 } 493 494 function showDetailedOptions() { 495 document.getElementById('simple-options').style.display = 'none' 496 document.getElementById('detailed-options').style.display = 'block' 497 document.getElementById('consent-description').innerHTML = 498 'Our website uses cookies to distinguish you from other users of our website. This helps us provide you with a more personalized experience when you browse our website and also allows us to improve our site. Cookies may collect information that is used to tailor ads shown to you on our website and other websites. The information might be about you, your preferences or your device. The information does not usually directly identify you, but it can give you a more personalized web experience. You can choose not to allow some types of cookies.' 499 500 // Use async function to get preferences 501 getInitialConsentPreferences().then((currentPreferences) => { 502 if (currentPreferences) { 503 updateToggleStatus('analytics-consent', 'analytics-status', currentPreferences.analytics) 504 updateToggleStatus('marketing-consent', 'marketing-status', currentPreferences.marketing) 505 } 506 }) 507 } 508 509 function updateToggleStatus(elementId, statusElementId, checked) { 510 const element = document.getElementById(elementId) 511 const statusElement = document.getElementById(statusElementId) 512 if (element) element.checked = checked 513 if (statusElement) statusElement.textContent = checked ? 'On' : 'Off' 514 } 515 516 // Initialize on DOMContentLoaded 517 document.addEventListener('DOMContentLoaded', async function () { 518 // Get initial preferences 519 const initialPreferences = await getInitialConsentPreferences() 520 521 // Publish the effective verdict (cookie OR regional default) for the 522 // antalytics loader: implied consent in opt-out regions never writes a 523 // cookie, so a cookie-only reader would drop that traffic. 524 window.__anthropicEffectiveConsent = {...initialPreferences} 525 window.dispatchEvent(new CustomEvent('anthropic-consent-updated')) 526 527 // Set up dataLayer and gtag function 528 // NOTE: This must be initialized before any gtag() calls, regardless of consent preferences. 529 // The dataLayer array and gtag function are infrastructure that Google Consent Mode requires. 530 window.dataLayer = window.dataLayer || [] 531 function gtag() { 532 window.dataLayer.push(arguments) 533 } 534 window.gtag = gtag 535 536 // Set Google Consent Mode defaults based on user preferences 537 // This is called regardless of whether GTM loads, as other Google tags may use these signals. 538 // Each consent type is set based on the corresponding user preference. 539 gtag('consent', 'default', { 540 ad_personalization: initialPreferences.marketing ? 'granted' : 'denied', 541 ad_user_data: initialPreferences.marketing ? 'granted' : 'denied', 542 ad_storage: initialPreferences.marketing ? 'granted' : 'denied', 543 analytics_storage: initialPreferences.analytics ? 'granted' : 'denied', 544 functionality_storage: 'granted', 545 personalization_storage: initialPreferences.marketing ? 'granted' : 'denied', 546 security_storage: 'granted', 547 }) 548 549 // Conditionally load Google Tag Manager based on marketing consent 550 // GDPR COMPLIANCE NOTE: Per German Administrative Court of Hanover ruling (March 2025), 551 // GTM transmits user data (IP, device info) to Google servers immediately on load. 552 // This constitutes personal data processing under GDPR and requires prior consent. 553 // GTM is only loaded after explicit user consent to comply with EU privacy regulations. 554 // Reference: https://www.didomi.io/blog/google-tag-manager-gtm-consent-2025-germany 555 if (initialPreferences.marketing) { 556 // Load GTM (claude.com container: GTM-NRG742MW) 557 ;(function (w, d, s, l, i) { 558 w[l] = w[l] || [] 559 w[l].push({'gtm.start': new Date().getTime(), 'event': 'gtm.js'}) 560 var f = d.getElementsByTagName(s)[0], 561 j = d.createElement(s), // nosemgrep: create-script-element 562 dl = l != 'dataLayer' ? '&l=' + l : '' 563 j.async = true 564 j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl 565 f.parentNode.insertBefore(j, f) 566 })(window, document, 'script', 'dataLayer', 'GTM-NRG742MW') // nosemgrep: create-script-element 567 } 568 569 // Initialize Webflow Optimize consent based on analytics preference 570 if (typeof window.wf !== 'undefined' && typeof window.wf.ready === 'function') { 571 window.wf.ready(() => { 572 updateWebflowOptimizeConsent(initialPreferences) 573 }) 574 } 575 576 // Show banner if no cookie exists and user is in explicit consent country 577 // Skip banner if GPC is enabled (user has already expressed opt-out preference) 578 if (!cookieUtils.has(CONSENT_PREFERENCES_COOKIE_KEY) && !isGlobalPrivacyControlEnabled()) { 579 const requiresExplicitConsent = await inExplicitConsentRequiredCountry() 580 if (requiresExplicitConsent) { 581 showBanner() 582 } 583 } 584 585 // Event listeners 586 const acceptBtn = document.getElementById('accept-btn') 587 if (acceptBtn) { 588 acceptBtn.addEventListener('click', () => { 589 saveConsentPreferences({analytics: true, marketing: true}) 590 }) 591 } 592 593 const rejectBtn = document.getElementById('reject-btn') 594 if (rejectBtn) { 595 rejectBtn.addEventListener('click', () => {
596 saveConsentPreferences({analytics: false, marketing: false}) 597 }) 598 } 599 600 const customizeBtn = document.getElementById('customize-btn') 601 if (customizeBtn) { 602 customizeBtn.addEventListener('click', showDetailedOptions) 603 } 604 605 const savePreferencesBtn = document.getElementById('save-preferences-btn') 606 if (savePreferencesBtn) { 607 savePreferencesBtn.addEventListener('click', () => { 608 const analyticsConsent = document.getElementById('analytics-consent') 609 const marketingConsent = document.getElementById('marketing-consent') 610 611 if (analyticsConsent && marketingConsent) { 612 saveConsentPreferences({ 613 analytics: analyticsConsent.checked, 614 marketing: marketingConsent.checked, 615 }) 616 } 617 }) 618 } 619 620 const privacyChoicesBtn = document.getElementById('privacy-choices-btn') 621 if (privacyChoicesBtn) { 622 privacyChoicesBtn.addEventListener('click', showBanner) 623 } 624 625 // Toggle status updates 626 ;['analytics-consent', 'marketing-consent'].forEach((id) => { 627 const element = document.getElementById(id) 628 if (element) { 629 element.addEventListener('change', function () { 630 const statusId = id.replace('-consent', '-status') 631 const statusElement = document.getElementById(statusId) 632 if (statusElement) { 633 statusElement.textContent = this.checked ? 'On' : 'Off' 634 } 635 }) 636 } 637 }) 638 639 // Escape key handler 640 document.addEventListener('keydown', function (event) { 641 const dialog = document.getElementById('consent-container') 642 if (event.key === 'Escape' && dialog?.open) { 643 hideBanner() 644 } 645 }) 646 }) 647})() 648 649/** 650 * YouTube Privacy Compliance 651 * Converts YouTube embeds to use youtube-nocookie.com for GDPR compliance. 652 * This prevents YouTube from setting tracking cookies until the user plays the video. 653 * 654 * Note: This uses a one-time check on page load rather than a MutationObserver. 655 * Dynamically loaded YouTube embeds (e.g., from user interactions) are already 656 * handled at the source to use nocookies URLs. A MutationObserver would add 657 * continuous performance overhead for minimal benefit. 658 */ 659;(function () { 660 'use strict' 661 662 function convertToNoCookie(url) { 663 if (!url || url.includes('youtube-nocookie.com')) return url 664 665 // Only convert valid HTTPS YouTube embed URLs (prevents XSS via javascript: URLs) 666 if (!/^https?:\/\/(?:www\.)?youtube\.com\/embed\//i.test(url)) return url 667 668 return url.replace(/(?:www\.)?youtube\.com/g, 'www.youtube-nocookie.com') 669 } 670 671 window.addEventListener('load', function () { 672 try { 673 // Match both src and data-src (for lazy-loaded iframes), with or without www prefix 674 var selector = 'iframe[src*="youtube.com/embed"], iframe[data-src*="youtube.com/embed"]' 675 document.querySelectorAll(selector).forEach(function (iframe) { 676 if (iframe.src) { 677 iframe.src = convertToNoCookie(iframe.src) 678 } 679 if (iframe.dataset.src) { 680 iframe.dataset.src = convertToNoCookie(iframe.dataset.src) 681 } 682 }) 683 } catch (e) { 684 // Silently fail - this is a privacy enhancement, not critical functionality 685 } 686 }) 687})()
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.