/** * Client-side Guide Router * ======================== * Fetches /inc/config/guide-routing.json once per session and resolves * the best-matched guide for the current URL. * * Deploy to: /home/petronellatech/public_html/inc/js/guide-routing.js * * Exposes: window.PTGGuide = { * resolve: function(url) -> Promise, * current: function() -> Promise * }; * * Consumed by: default-bottom.tpl slide-in + /inc/exit-popup.js */ (function () { 'use strict'; var CACHE_KEY = 'ptg-guide-routing-v1'; var CACHE_TTL = 6 * 60 * 60 * 1000; // 6 hours var CONFIG_URL = '/inc/config/guide-routing.json'; var inflight = null; function loadRouting() { if (inflight) return inflight; try { var cached = sessionStorage.getItem(CACHE_KEY); if (cached) { var parsed = JSON.parse(cached); if (parsed && parsed._ts && (Date.now() - parsed._ts) < CACHE_TTL && parsed.data) { inflight = Promise.resolve(parsed.data); return inflight; } } } catch (e) { /* ignore */ } inflight = fetch(CONFIG_URL, { credentials: 'omit' }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (data) { if (!data || !data.guides || !data.routing) return null; try { sessionStorage.setItem(CACHE_KEY, JSON.stringify({ _ts: Date.now(), data: data })); } catch (e) { /* ignore quota */ } return data; }) .catch(function () { return null; }); return inflight; } function matchGuideId(url, routing) { var u = (url || '/').toLowerCase(); var best = null; var fallback = null; for (var i = 0; i < routing.length; i++) { var r = routing[i]; var pat = (r.match_pattern || '').toLowerCase(); var t = r.match_type || ''; var pri = +r.priority || 0; if (t === 'default') { fallback = r; continue; } if (!pat) continue; var hit = false; if (t === 'url_prefix') hit = u.indexOf(pat) === 0; else if (t === 'url_contains') hit = u.indexOf(pat) !== -1; else if (t === 'url_regex') { try { hit = new RegExp(pat, 'i').test(u); } catch (e) {} } if (hit && (!best || pri > (+best.priority || 0))) best = r; } return (best && best.guide_id) || (fallback && fallback.guide_id) || 'business-tech-2026'; } function resolve(url) { return loadRouting().then(function (data) { if (!data) return defaultGuide(); var gid = matchGuideId(url, data.routing); var g = data.guides[gid] || data.guides['business-tech-2026'] || defaultGuide(); g = Object.assign({}, g, { guide_id: gid }); return g; }); } function defaultGuide() { return { guide_id: 'business-tech-2026', title: '2026 SMB Cybersecurity Survival Guide', landing_page_url: 'https://petronellatech.com/2026-guide/', download_url: 'https://petronellatech.com/2026-guide/download.php', popup_heading: 'Before you go…', popup_body: 'Get the 2026 SMB Cybersecurity Survival Guide — free, 28 pages of actionable strategies.', popup_cta_text: 'Download Free Guide', pages: 28 }; } // ── A/B Variant Assignment (Workstream A5) ──────────────────────── // Stable per (session, guide_id). Cookie: ptg_pop_var_. // 33/33/33 split across control / treatment-a / treatment-b. var VARIANTS = ['control', 'treatment-a', 'treatment-b']; function readCookie(name) { var m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.$?*|{}()[\]\\/+^]/g, '\\$&') + '=([^;]*)')); return m ? decodeURIComponent(m[1]) : ''; } function writeCookie(name, value, days) { var exp = new Date(Date.now() + days * 86400000).toUTCString(); document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + exp + '; path=/; SameSite=Lax'; } function pickVariant() { // Prefer crypto for a fair split; fall back to Math.random. if (window.crypto && window.crypto.getRandomValues) { var buf = new Uint32Array(1); window.crypto.getRandomValues(buf); return VARIANTS[buf[0] % VARIANTS.length]; } return VARIANTS[Math.floor(Math.random() * VARIANTS.length)]; } function variantFor(guideId) { var gid = guideId || 'unknown'; var cookieName = 'ptg_pop_var_' + gid.replace(/[^A-Za-z0-9_-]/g, '_'); var existing = readCookie(cookieName); if (VARIANTS.indexOf(existing) !== -1) return existing; var v = pickVariant(); writeCookie(cookieName, v, 30); return v; } // ── Session ID cookie (for popup event dedup / funnel joins) ───── function sessionId() { var existing = readCookie('ptg_pop_sid'); if (/^[a-f0-9]{32}$/.test(existing)) return existing; var sid; if (window.crypto && window.crypto.getRandomValues) { var arr = new Uint8Array(16); window.crypto.getRandomValues(arr); sid = Array.prototype.map.call(arr, function (b) { return ('0' + b.toString(16)).slice(-2); }).join(''); } else { sid = ''; while (sid.length < 32) sid += Math.floor(Math.random() * 16).toString(16); } writeCookie('ptg_pop_sid', sid, 30); return sid; } window.PTGGuide = { resolve: resolve, current: function () { return resolve(window.location.pathname + window.location.search); }, variant: variantFor, sessionId: sessionId, _matchGuideId: matchGuideId }; })();