PageSourceSearch

https://jettseo.com/assets/js/consent.js?v=1

js jettseo.com collected 2026-10-06 19:47:52 UTC 14,141 bytes, 339 lines download raw bytes

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.