1/** 2 * jettseo.com â consent manager. 3 * 4 * The rule this file exists to enforce: nothing that is not strictly necessary 5 * touches the network or storage until the visitor has actively opted in. 6 * That means no Google Analytics request, no StatInside request, no Vimeo 7 * iframe and no Calendly script before a stored "granted" for the matching 8 * category. Consent Mode defaults are set here rather than inline in <head> 9 * precisely so that no Google endpoint is contacted at all beforehand. 10 * 11 * Storage: one first-party cookie, jettseo_consent, holding 12 * {v: <schema version>, ts: <unix seconds>, cats: {analytics: bool, external: bool}} 13 * Lifetime is 6 months, the period CNIL treats as a reasonable maximum before a 14 * choice should be asked again. A refusal is stored just like an acceptance, so 15 * saying no is remembered instead of being re-prompted on every page. 16 * 17 * Public API (also used by main.js): window.jettseoConsent 18 * .has(category) -> bool 19 * .get() -> {analytics, external} 20 * .grant(category) -> add one category (used by the click-to-load embeds) 21 * .open() -> open the preferences dialog 22 * .onChange(fn) -> called with the state whenever it changes 23 */ 24(function () { 25 'use strict'; 26 27 var COOKIE = 'jettseo_consent'; 28 var VERSION = 1; // bump to invalidate every stored choice 29 var MAX_AGE = 60 * 60 * 24 * 183; // 6 months, in seconds 30 31 var OPTIONAL = ['analytics', 'external']; 32 33 // Measurement IDs come from $ANALYTICS in includes/config.php, handed over 34 // as data-attributes on this script tag. An empty ID means that tool is off. 35 var cfgEl = document.querySelector('script[data-ga4-id]') || {}; 36 var cfg = cfgEl.dataset || {}; 37 var GA4_ID = cfg.ga4Id || ''; 38 var CLARITY_ID = cfg.clarityId || ''; 39 40 // ---------------------------------------------------------------- storage 41 42 function readCookie(name) { 43 var parts = ('; ' + document.cookie).split('; ' + name + '='); 44 return parts.length >= 2 ? parts.pop().split(';').shift() : null; 45 } 46 47 function writeCookie(name, value, maxAge) { 48 // Secure is safe to always set: the site is HTTPS-only and redirects 49 // port 80. Lax keeps the cookie on normal top-level navigations. 50 document.cookie = name + '=' + value + 51 '; path=/; max-age=' + maxAge + 52 '; SameSite=Lax' + (location.protocol === 'https:' ? '; Secure' : ''); 53 } 54 55 function deleteCookie(name, domain) { 56 var base = name + '=; path=/; max-age=0; SameSite=Lax'; 57 document.cookie = base; 58 if (domain) document.cookie = base + '; domain=' + domain; 59 } 60 61 /** Stored choice, or null when the visitor has never answered. */ 62 function stored() { 63 var raw = readCookie(COOKIE); 64 if (!raw) return null; 65 try { 66 var data = JSON.parse(decodeURIComponent(raw)); 67 if (!data || data.v !== VERSION || !data.cats) return null; // schema changed -> ask again 68 return data; 69 } catch (e) { 70 return null; 71 } 72 } 73 74 var state = { analytics: false, external: false }; 75 var listeners = []; 76 var applied = {}; // categories whose scripts we already started 77 78 function persist() { 79 writeCookie(COOKIE, encodeURIComponent(JSON.stringify({ 80 v: VERSION, 81 ts: Math.floor(Date.now() / 1000), 82 cats: { analytics: state.analytics, external: state.external } 83 })), MAX_AGE); 84 } 85 86 // ------------------------------------------------------- Google Consent Mode 87 88 window.dataLayer = window.dataLayer || []; 89 function gtag() { window.dataLayer.push(arguments); } 90 91 // Everything denied up front. This queues locally; it sends nothing, 92 // because gtag.js itself is not on the page yet. 93 gtag('consent', 'default', { 94 ad_storage: 'denied', 95 ad_user_data: 'denied', 96 ad_personalization: 'denied', 97 analytics_storage: 'denied', 98 functionality_storage: 'denied', 99 personalization_storage:'denied', 100 security_storage: 'granted', 101 wait_for_update: 500 102 }); 103 gtag('set', 'ads_data_redaction', true); 104 gtag('set', 'url_passthrough', false); 105 106 // ------------------------------------------------------------ loaders 107 108 function injectScript(src, attrs) { 109 var s = document.createElement('script'); 110 s.src = src; 111 s.async = true;
112 if (attrs) Object.keys(attrs).forEach(function (k) { s.setAttribute(k, attrs[k]); }); 113 document.head.appendChild(s); 114 return s; 115 } 116 117 var loaders = { 118 analytics: function () { 119 gtag('consent', 'update', { analytics_storage: 'granted' }); 120 121 if (GA4_ID) { 122 injectScript('https://www.googletagmanager.com/gtag/js?id=' + GA4_ID); 123 gtag('js', new Date()); 124 gtag('config', GA4_ID, { 125 anonymize_ip: true, // no-op in GA4, kept as an explicit signal 126 allow_google_signals: false, // no cross-device ads/demographics 127 allow_ad_personalization_signals: false, 128 cookie_flags: 'SameSite=Lax;Secure', 129 cookie_expires: 60 * 60 * 24 * 396 // 13 months, the CNIL ceiling for analytics 130 }); 131 } 132 133 // Microsoft Clarity â heatmaps and session replay. Far more invasive 134 // than page counting, so it is gated exactly like the rest and, in 135 // addition, told about the consent through Clarity's own API. 136 if (CLARITY_ID) { 137 (function (c, l, a, r, i, t, y) { 138 c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments); }; 139 t = l.createElement(r); t.async = 1; t.src = 'https://www.clarity.ms/tag/' + i; 140 y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y); 141 })(window, document, 'clarity', 'script', CLARITY_ID); 142 window.clarity('consent'); 143 } 144 145 // StatInside â same category, same gate. It sets a one-year visitor 146 // id and records mouse/scroll/click activity, so it may never run 147 // ahead of consent. 148 window._siData = window._siData || []; 149 window.statInside = function () { window._siData.push(arguments); }; 150 injectScript('https://statinside.com/counter.js', { 'data-statinside-id': '6' }); 151 }, 152 153 external: function () { 154 unlockEmbeds(); 155 } 156 }; 157 158 /** Cookies to clear when a category is switched back off. */ 159 var cleanup = { 160 analytics: function () { 161 gtag('consent', 'update', { analytics_storage: 'denied' }); 162 if (window.clarity) { try { window.clarity('consent', false); } catch (e) {} } 163 var host = location.hostname.replace(/^www\./, ''); 164 ['_ga', '_ga_' + GA4_ID.replace(/^G-/, ''), '_gid', '_gat', 165 'si_usr_id', 'si_ses_id', '_clck', '_clsk', 'CLID', 'MUID'] 166 .forEach(function (n) { 167 deleteCookie(n); 168 deleteCookie(n, '.' + host); 169 deleteCookie(n, host); 170 }); 171 // si_ab_* names are generated per test, so sweep by prefix. 172 document.cookie.split('; ').forEach(function (pair) { 173 var n = pair.split('=')[0]; 174 if (n.indexOf('si_ab_') === 0) { deleteCookie(n); deleteCookie(n, '.' + host); } 175 }); 176 }, 177 external: function () {} 178 }; 179 180 // ------------------------------------------------------- embed unblocking 181 182 /** 183 * Swap every blocked placeholder for its real iframe. The markup carries the 184 * URL in data-cc-src and renders a placeholder until this runs, so the 185 * third-party request genuinely never happens without consent. 186 */ 187 function unlockEmbeds() { 188 var nodes = document.querySelectorAll('[data-cc-embed]'); 189 Array.prototype.forEach.call(nodes, function (node) { 190 if (node.dataset.ccLoaded) return; 191 var iframe = document.createElement('iframe'); 192 iframe.src = node.dataset.ccSrc; 193 iframe.title = node.dataset.ccTitle || ''; 194 if (node.dataset.ccLabel) iframe.setAttribute('aria-label', node.dataset.ccLabel); 195 iframe.setAttribute('loading', 'lazy'); 196 iframe.setAttribute('allow', 'autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media; web-share'); 197 iframe.setAttribute('allowfullscreen', ''); 198 iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); 199 node.innerHTML = ''; 200 node.appendChild(iframe); 201 node.dataset.ccLoaded = '1'; 202 node.classList.add('cc-embed-loaded'); 203 }); 204 } 205 206 // ------------------------------------------------------------ apply / notify 207 208 function apply(previous) {
209 OPTIONAL.forEach(function (cat) { 210 if (state[cat] && !applied[cat]) { 211 applied[cat] = true; 212 loaders[cat](); 213 } 214 }); 215 216 // Withdrawal must actually stop the processing, not just stop future 217 // page views. Clear the cookies, then reload so already-running 218 // third-party code is gone from the page. 219 var revoked = OPTIONAL.some(function (cat) { 220 return previous && previous[cat] && !state[cat]; 221 }); 222 if (revoked) { 223 OPTIONAL.forEach(function (cat) { 224 if (previous[cat] && !state[cat]) cleanup[cat](); 225 }); 226 location.reload(); 227 return; 228 } 229 230 listeners.forEach(function (fn) { try { fn(getState()); } catch (e) {} }); 231 } 232 233 function getState() { return { analytics: state.analytics, external: state.external }; } 234 235 function setState(next, options) { 236 var previous = getState(); 237 OPTIONAL.forEach(function (cat) { state[cat] = !!next[cat]; }); 238 persist(); 239 syncToggles(); 240 if (!options || !options.silent) hideBanner(); 241 apply(previous); 242 } 243 244 // ------------------------------------------------------------------- UI 245 246 var banner = document.getElementById('ccBanner'); 247 var modal = document.getElementById('ccModal'); 248 249 function showBanner() { if (banner) banner.hidden = false; } 250 function hideBanner() { if (banner) banner.hidden = true; } 251 252 function syncToggles() { 253 if (!modal) return; 254 OPTIONAL.forEach(function (cat) { 255 var input = modal.querySelector('[data-cc-category="' + cat + '"]'); 256 if (input) input.checked = !!state[cat]; 257 }); 258 } 259 260 function openPrefs() { 261 if (!modal) return; 262 syncToggles(); 263 if (typeof modal.showModal === 'function') { 264 if (!modal.open) modal.showModal(); 265 } else { 266 modal.setAttribute('open', ''); // very old browsers: inline fallback 267 } 268 } 269 270 function closePrefs() { 271 if (!modal) return; 272 if (typeof modal.close === 'function' && modal.open) modal.close(); 273 else modal.removeAttribute('open'); 274 } 275 276 function readToggles() { 277 var next = {}; 278 OPTIONAL.forEach(function (cat) { 279 var input = modal && modal.querySelector('[data-cc-category="' + cat + '"]'); 280 next[cat] = !!(input && input.checked); 281 }); 282 return next; 283 } 284 285 function all(value) { 286 var next = {}; 287 OPTIONAL.forEach(function (cat) { next[cat] = value; }); 288 return next; 289 } 290 291 // One delegated handler covers the banner, the dialog and the footer link. 292 document.addEventListener('click', function (e) { 293 var trigger = e.target.closest ? e.target.closest('[data-cc-action]') : null; 294 if (!trigger) return; 295 var action = trigger.getAttribute('data-cc-action'); 296 297 if (action === 'accept') { e.preventDefault(); setState(all(true)); closePrefs(); } 298 else if (action === 'reject') { e.preventDefault(); setState(all(false)); closePrefs(); } 299 else if (action === 'save') { e.preventDefault(); setState(readToggles()); closePrefs(); } 300 else if (action === 'open-prefs') { e.preventDefault(); openPrefs(); } 301 else if (action === 'close-prefs') { e.preventDefault(); closePrefs(); } 302 else if (action === 'grant') { 303 // Click-to-load on a blocked embed: consent for that one category, 304 // given knowingly because the placeholder names the provider. 305 e.preventDefault(); 306 var cat = trigger.getAttribute('data-cc-grant') || 'external'; 307 var next = getState(); 308 next[cat] = true; 309 setState(next); 310 } 311 }); 312 313 // Dismissing the dialog (Esc, backdrop) must not count as agreement. If no 314 // choice exists yet, the banner comes back so the question stays open. 315 if (modal) { 316 modal.addEventListener('close', function () { if (!stored()) showBanner(); }); 317 modal.addEventListener('cancel', function () { if (!stored()) showBanner(); }); 318 } 319 320 // ------------------------------------------------------------------ boot 321 322 var saved = stored(); 323 if (saved) {
324 OPTIONAL.forEach(function (cat) { state[cat] = !!saved.cats[cat]; }); 325 syncToggles(); 326 apply(null); // start what was already agreed to 327 } else { 328 syncToggles(); // leaves every optional toggle unchecked 329 showBanner(); 330 } 331 332 window.jettseoConsent = { 333 has: function (cat) { return cat === 'necessary' ? true : !!state[cat]; }, 334 get: getState, 335 grant: function (cat) { var n = getState(); n[cat] = true; setState(n); }, 336 open: openPrefs, 337 onChange: function (fn) { if (typeof fn === 'function') listeners.push(fn); } 338 }; 339})();
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.