1/* public/js/ads.js â ad lifecycle manager. 2 * 3 * Two responsibilities: 4 * 1) Fire adsbygoogle.push({}) for every <ins class="adsbygoogle" 5 * data-ad-push-pending="1"> AFTER window.load. This replaces the 6 * inline push script AdSense normally wants next to every <ins>, 7 * because that inline push fires mid-parse â at which point any 8 * <ins> inside a position:fixed sticky wrapper measures 9 * offsetWidth=0 and AdSense aborts with "No slot size for 10 * availableWidth=0". Waiting for load guarantees stable layout. 11 * 12 * 2) For sticky/float wrappers: measure the rendered height, set it 13 * as a CSS custom property (--ad-sticky-bottom-h / --ad-sticky-top-h) 14 * and add a body class so ads.css can reserve exact page padding. 15 * This keeps the footer and page content visible â the ad pushes 16 * content out of its way rather than overlaying it. 17 */ 18(function () { 19 'use strict'; 20 21 // ------------------------------------------------------------------ 22 // 1. Fire the deferred adsbygoogle pushes 23 // ------------------------------------------------------------------ 24 25 function pushPending() { 26 var units = document.querySelectorAll( 27 'ins.adsbygoogle[data-ad-push-pending="1"]' 28 ); 29 for (var i = 0; i < units.length; i++) { 30 var ins = units[i]; 31 ins.removeAttribute('data-ad-push-pending'); 32 try { 33 (window.adsbygoogle = window.adsbygoogle || []).push({}); 34 } catch (e) { 35 if (window.console) console.warn('ad push failed:', e); 36 } 37 // Start watching for unfilled so we can swap in backfill HTML 38 // if the unit has any. Cheap if the unit has no backfill block. 39 watchForBackfill(ins); 40 } 41 } 42 43 if (document.readyState === 'complete') { 44 setTimeout(pushPending, 0); 45 } else { 46 window.addEventListener('load', pushPending); 47 } 48 49 // ------------------------------------------------------------------ 50 // Backfill â show admin-supplied house ad when AdSense doesn't fill. 51 // 52 // *** DO NOT REINTRODUCE display:none ON THE <ins>. *** 53 // 54 // This used to set ins.style.display='none' as soon as it decided to 55 // show the backfill, including on the 3.5s backstop â i.e. while the ad 56 // request was very often STILL IN FLIGHT. AdSense then found a hidden 57 // tag and logged "No ad requested because of display:none on the 58 // adsbygoogle tag", so the unit could never fill, so the house backfill 59 // won permanently. The slot was unfillable by construction. 60 // 61 // The rule: hiding the <ins> before AdSense has resolved the request 62 // cancels the request. So we never hide it at all. The <ins> stays in 63 // the layout for the whole page lifetime, which also means a later 64 // AdSense refresh can still fill it. 65 // 66 // Decision logic: 67 // - data-ad-status="filled" â hide the backfill (ad wins) 68 // - data-ad-status="unfilled" â show the backfill (definitive: the 69 // request completed, nothing to show) 70 // - still no status at 3.5s â show the backfill PROVISIONALLY, but 71 // leave the <ins> alone and keep 72 // watching. If the ad lands late, the 73 // backfill is hidden again. 74 // 75 // The backfill is absolutely positioned over the <ins> (see ads.css), so 76 // both states occupy the same reserved space and nothing reflows. The 77 // <ins> only gets pointer-events:none while the backfill is up, so 78 // clicks reach the house ad rather than an empty ad slot. 79 // 80 // Status is watched with a MutationObserver on the attribute plus a 81 // polling fallback, so a late fill is noticed either way. 82 // 83 // Beacons fire once on reveal (impression) and on click (anywhere inside 84 // the backfill block). GDPR-safe: no cookies, no fingerprinting, no PII â 85 // the endpoint just increments two integers on the adsense_units row. 86 // ------------------------------------------------------------------ 87 88 function findBackfill(ins) { 89 var wrap = ins.closest('.ad-sticky, .ad-float, .ad-inline'); 90 if (!wrap || !wrap.classList.contains('ad-has-backfill')) return null; 91 var node = wrap.querySelector('.ad-backfill'); 92 return node || null; 93 } 94 95 function sendBeacon(action, slug) { 96 if (!slug) return; 97 var url = '/ads-track.php?action=' + encodeURIComponent(action) 98 + '&unit=' + encodeURIComponent(slug); 99 try { 100 if (navigator.sendBeacon) { 101 // sendBeacon needs a body for the request to be POST. 102 navigator.sendBeacon(url, new Blob([''], { type: 'application/x-www-form-urlencoded' })); 103 } else { 104 // Fallback for very old browsers; fire-and-forget. 105 fetch(url, { method: 'POST', keepalive: true, credentials: 'omit' }).catch(function () {}); 106 } 107 } catch (e) { /* never let analytics break the page */ } 108 } 109 110 function showBackfill(ins, backfill) { 111 if (!backfill.hasAttribute('hidden')) return; // already visible 112 backfill.removeAttribute('hidden'); 113 114 // NOTE: the <ins> deliberately keeps display:block here. Only its 115 // interactivity is suppressed, so clicks land on the house ad while 116 // the slot itself stays alive and refreshable. 117 ins.style.pointerEvents = 'none'; 118 119 var slug = backfill.getAttribute('data-ad-backfill-slug') || ''; 120 121 // Impression fires once per page view even if the backfill is hidden 122 // and shown again by a late fill / later unfill. 123 if (!backfill.hasAttribute('data-ad-impression-sent')) { 124 backfill.setAttribute('data-ad-impression-sent', '1'); 125 sendBeacon('impression', slug); 126 } 127 // Delegate clicks anywhere inside the backfill â fires the click 128 // beacon then lets the original link / handler proceed normally. 129 if (!backfill.hasAttribute('data-ad-click-bound')) { 130 backfill.setAttribute('data-ad-click-bound', '1'); 131 backfill.addEventListener('click', function () { sendBeacon('click', slug); }, { passive: true }); 132 } 133 } 134 135 function hideBackfill(ins, backfill) { 136 if (backfill.hasAttribute('hidden')) return; // already hidden 137 backfill.setAttribute('hidden', ''); 138 ins.style.pointerEvents = ''; 139 } 140 141 function watchForBackfill(ins) { 142 var backfill = findBackfill(ins); 143 if (!backfill) return; // no backfill configured for this unit 144 145 var t0 = Date.now(); 146 var REVEAL_MS = 3500; // provisional reveal while still pending 147 var GIVE_UP_MS = 30000; // stop watching for a late fill 148 var POLL_MS = 250; 149 150 function evaluate() { 151 var status = ins.getAttribute('data-ad-status'); 152 if (status === 'filled') { hideBackfill(ins, backfill); return; } 153 if (status === 'unfilled') { showBackfill(ins, backfill); return; } 154 // No status yet â the request may still be in flight. Show the 155 // house ad so the space isn't blank, but do NOT touch the <ins>. 156 if ((Date.now() - t0) >= REVEAL_MS) showBackfill(ins, backfill); 157 } 158 159 evaluate(); 160 161 var mo = null; 162 if (window.MutationObserver) { 163 mo = new MutationObserver(evaluate); 164 mo.observe(ins, { attributes: true, attributeFilter: ['data-ad-status'] }); 165 } 166 167 // Polling fallback â also bounds how long we keep watching. 168 var iv = setInterval(function () { 169 evaluate(); 170 if ((Date.now() - t0) >= GIVE_UP_MS) { 171 clearInterval(iv); 172 if (mo) mo.disconnect(); 173 } 174 }, POLL_MS); 175 } 176 177 // ------------------------------------------------------------------ 178 // 2. Measure sticky wrappers and reserve page padding 179 // ------------------------------------------------------------------ 180 181 function measure(wrap, varName, bodyClass) { 182 if (!wrap) return; 183 184 var sync = function () { 185 var cs = window.getComputedStyle(wrap); 186 if (cs.display === 'none' || wrap.hasAttribute('hidden')) { 187 document.body.classList.remove(bodyClass); 188 document.documentElement.style.setProperty(varName, '0px'); 189 return; 190 } 191 var h = wrap.offsetHeight; 192 if (!h) { 193 document.body.classList.remove(bodyClass); 194 document.documentElement.style.setProperty(varName, '0px'); 195 return; 196 } 197 document.documentElement.style.setProperty(varName, h + 'px'); 198 document.body.classList.add(bodyClass); 199 }; 200 201 sync(); 202 203 if (window.ResizeObserver) { 204 new ResizeObserver(sync).observe(wrap); 205 } else { 206 window.addEventListener('resize', sync); 207 } 208 209 var attempts = 0; 210 var poll = setInterval(function () { 211 sync(); 212 if (++attempts > 10) clearInterval(poll); 213 }, 500); 214 } 215 216 function initSticky() { 217 measure( 218 document.querySelector('.ad-sticky--bottom'),
219 '--ad-sticky-bottom-h', 220 'has-ad-sticky-bottom' 221 ); 222 measure( 223 document.querySelector('.ad-sticky--top'), 224 '--ad-sticky-top-h', 225 'has-ad-sticky-top' 226 ); 227 } 228 229 if (document.readyState === 'loading') { 230 document.addEventListener('DOMContentLoaded', initSticky); 231 } else { 232 initSticky(); 233 } 234})();
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.