1const publicPages = new Set([ 2 '/', '/research/', '/team/', '/about-creative-strategies/', '/analysis-services/', 3 '/market-intelligence/', '/strategic-investement-insights-program/', 4 '/strategic-investment-insights-program/', '/creative-strategies-history-in-silicon-valley/', 5 '/delight-scale/', '/what-is-the-delight-scale/', '/why-use-the-delight-scale/' 6]); 7const reservedSegments = new Set([ 8 'ghost', 'admin', 'wp-admin', 'wp-json', 'api', 'members', 'member', 'account', 9 'accounts', 'portal', 'signin', 'sign-in', 'signup', 'sign-up', 'login', 'logout', 10 'signout', 'sign-out', 'register', 'subscribe', 'unsubscribe', 'subscription', 11 'subscriptions', 'membership', 'checkout', 'billing', 'payment', 'payments', 12 'auth', 'authentication', 'reset-password', 'forgot-password', 'preview', 'private', 13 'rss', 'feed', 'feeds', 'index', 'research-index', 'search-index', 'search', 14 'contact', 'contact-us', 'newsletter', 'join-the-newsletter', 'form', 'forms', 15 'download', 'downloads', 'cdn-cgi', 'content', 'assets', 'sitemap', 'sitemaps' 16]); 17const optOutSelector = '[data-no-prefetch], [data-prefetch="false"], [data-no-speculation], .no-prefetch, .do-not-prefetch, [data-portal], [data-ghost-search], [hidden], [inert], [aria-hidden="true"], form'; 18const dwellMs = 200; 19const maxPrefetches = 4; 20const maxActivePrefetches = 2; 21const lifetimeMs = 30_000; 22const initialized = new WeakMap(); 23 24export function publicNavigationUrl(link, location) { 25 if (!link || link.isConnected === false || link.hasAttribute('download') || link.closest(optOutSelector)) return null; 26 const target = (link.getAttribute('target') || '').toLowerCase(); 27 const rel = (link.getAttribute('rel') || '').toLowerCase().split(/\s+/); 28 if ((target && target !== '_self') || rel.some(value => value === 'external' || value === 'nofollow')) return null; 29 const href = link.getAttribute('href'); 30 if (!href || /[?#%\\\s]/.test(href)) return null; 31 32 let url;
33 let current; 34 try { 35 current = new URL(location.href); 36 url = new URL(href, link.baseURI || current); 37 } catch { return null; } 38 if (!['http:', 'https:'].includes(url.protocol) || url.origin !== current.origin || 39 url.username || url.password || url.search || url.hash) return null; 40 41 const path = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`; 42 const currentPath = current.pathname.endsWith('/') ? current.pathname : `${current.pathname}/`; 43 if (path === currentPath || !/^\/(?:[a-z0-9-]+\/)*$/.test(path)) return null; 44 const segments = path.split('/').filter(Boolean); 45 if (segments.some(segment => reservedSegments.has(segment))) return null; 46 const researchPage = /^\/research\/(?:[a-z0-9-]+\/|page\/[1-9]\d*\/)$/.test(path); 47 const legacyArticle = segments.length === 1 && 48 Boolean(link.closest('[data-research-source="cs"]') || link.matches('a.report-card')); 49 return publicPages.has(path) || researchPage || legacyArticle ? url.href : null; 50} 51 52export function canWarmNavigation(document, navigator = {}) { 53 const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; 54 return document.visibilityState !== 'hidden' && !document.prerendering && navigator.onLine !== false && 55 !connection?.saveData && !['slow-2g', '2g', '3g'].includes(connection?.effectiveType) && 56 !(connection?.downlink > 0 && connection.downlink < 1.5); 57} 58 59export function initializeNavigationPrefetch(document, window) { 60 if (initialized.has(document)) return initialized.get(document); 61 const prefetched = new Set(); 62 const active = new Map(); 63 const pending = new Map(); 64 const listeners = []; 65 let researchIntent = false; 66 const supportsPrefetch = window.HTMLScriptElement?.supports?.('speculationrules') === true; 67 68 function removePrefetch(url) { 69 const entry = active.get(url); 70 if (!entry) return; 71 window.clearTimeout(entry.timer); 72 entry.script.remove(); 73 active.delete(url); 74 } 75 76 function cancelPending(source) { 77 window.clearTimeout(pending.get(source)?.timer); 78 pending.delete(source); 79 } 80 81 function cancelAll() { 82 for (const source of pending.keys()) cancelPending(source); 83 for (const url of active.keys()) removePrefetch(url); 84 } 85 86 function approach(link, source) { 87 if (!canWarmNavigation(document, window.navigator)) return; 88 const url = publicNavigationUrl(link, window.location); 89 if (!url) return; 90 if (!researchIntent && new URL(url).pathname.replace(/\/$/, '') === '/research') { 91 researchIntent = true; 92 document.dispatchEvent(new window.CustomEvent('cs:research-intent', {detail: {url, source}})); 93 } 94 if (!supportsPrefetch || prefetched.has(url) || prefetched.size >= maxPrefetches) return; 95 if (active.size >= maxActivePrefetches) removePrefetch(active.keys().next().value); 96 97 const script = document.createElement('script'); 98 script.type = 'speculationrules'; 99 script.dataset.csNavigationPrefetch = ''; 100 // This one-URL rule is inserted only after sustained hover/focus or a primary press. 101 // Prefetch downloads HTML without running the destination's analytics or member scripts. 102 script.textContent = JSON.stringify({prefetch: [{urls: [url], eagerness: 'immediate'}]}); 103 document.head.appendChild(script); 104 prefetched.add(url); 105 active.set(url, {script, timer: window.setTimeout(() => removePrefetch(url), lifetimeMs)}); 106 } 107 108 function anchor(target) { return target?.closest?.('a[href]'); } 109 function permittedEvent(event) { 110 return event.isTrusted !== false && !event.defaultPrevented && !event.altKey && !event.ctrlKey && 111 !event.metaKey && !event.shiftKey && event.isPrimary !== false && !(event.button > 0); 112 } 113 function schedule(event, source) { 114 if (!permittedEvent(event) || !canWarmNavigation(document, window.navigator)) return; 115 if (source === 'hover' && event.pointerType === 'touch') return; 116 const link = anchor(event.target); 117 if (!link || link === anchor(event.relatedTarget) || pending.get(source)?.link === link) return; 118 cancelPending(source); 119 if (!publicNavigationUrl(link, window.location)) return; 120 const timer = window.setTimeout(() => { 121 pending.delete(source); 122 approach(link, source); 123 }, dwellMs); 124 pending.set(source, {link, timer}); 125 } 126 function depart(event, source) { 127 const link = anchor(event.target); 128 if (pending.get(source)?.link === link && link !== anchor(event.relatedTarget)) cancelPending(source); 129 } 130 function press(event) { 131 if (!permittedEvent(event) || event.touches?.length > 1) return; 132 const link = anchor(event.target); 133 for (const source of pending.keys()) cancelPending(source); 134 approach(link, event.pointerType === 'touch' || event.type === 'touchstart' ? 'touch' : 'pointer'); 135 } 136 function listen(target, type, listener) { 137 target.addEventListener(type, listener, {passive: true}); 138 listeners.push(() => target.removeEventListener(type, listener)); 139 } 140 141 listen(document, window.PointerEvent ? 'pointerover' : 'mouseover', event => schedule(event, 'hover')); 142 listen(document, window.PointerEvent ? 'pointerout' : 'mouseout', event => depart(event, 'hover')); 143 listen(document, 'focusin', event => schedule(event, 'focus')); 144 listen(document, 'focusout', event => depart(event, 'focus')); 145 listen(document, window.PointerEvent ? 'pointerdown' : 'mousedown', press); 146 if (!window.PointerEvent) listen(document, 'touchstart', press); 147 listen(document, 'visibilitychange', () => { if (!canWarmNavigation(document, window.navigator)) cancelAll(); }); 148 const connection = window.navigator?.connection || window.navigator?.mozConnection || window.navigator?.webkitConnection; 149 if (connection?.addEventListener) listen(connection, 'change', () => { 150 if (!canWarmNavigation(document, window.navigator)) cancelAll(); 151 }); 152 listen(window, 'pagehide', () => { cancelAll(); prefetched.clear(); }); 153 listen(window, 'pageshow', event => { if (event.persisted) researchIntent = false; }); 154 155 const destroy = () => { 156 cancelAll();
157 listeners.forEach(remove => remove()); 158 initialized.delete(document); 159 }; 160 initialized.set(document, destroy); 161 return destroy; 162} 163 164if (typeof document !== 'undefined' && typeof window !== 'undefined') initializeNavigationPrefetch(document, window);
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.