PageSourceSearch

https://claude.com/shared/webflow-privacy-banner.js

js claude.com collected 2026-09-24 08:24:13 UTC 22,460 bytes, 687 lines download raw bytes

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.