PageSourceSearch

https://onlinecompass.net/js/ads.js

js onlinecompass.net collected 2026-10-02 06:55:00 UTC 11,543 bytes, 248 lines download raw bytes

1/*!
2 * Centralized AdSense loader for onlinecompass.net.
3 *
4 * Does three things, all from one deferred script:
5 *
6 *  1. DEFER THE AD LIBRARIES. The PubGuru header-bidding wrapper and the
7 *     AdSense loader are NOT placed in <head>. This script injects them only
8 *     AFTER the Cookiebot consent decision, at the next idle moment — so their
9 *     main-thread work (including Google's Funding Choices TCF wall) never
10 *     competes with first paint or the pre-consent interactions. This is the
11 *     main INP/LCP lever for an ad-supported page.
12 *     Order is preserved (PubGuru before AdSense) so bids are ready before slots
13 *     fill, and revenue/header-bidding are unaffected — ads simply load a moment
14 *     later, during idle time.
15 *
16 *  2. FIX THE ADSENSE TAG ERRORS the old per-slot inline push() scripts caused:
17 *     - "No slot size for availableWidth=0": push({}) fills the NEXT unfilled
18 *       <ins> in document order, so a hidden responsive variant (ad-mobile /
19 *       ad-tablet / ad-desktop, display:none on non-matching viewports) was
20 *       measured at width 0 and also stole a push meant for a visible slot.
21 *       Fixed by removing every hidden <ins> before any push runs.
22 *     - "All 'ins' elements ... already have ads in them": push({}) throws
23 *       this when no <ins> is left to fill — which happens when PubGuru
24 *       header-bidding or page-level auto-ads fill a slot out-of-band, so a
25 *       pure count cap can over-push by one. Fixed by also requiring an
26 *       unprocessed <ins> (no data-ad-status) before each push, with a
27 *       try/catch around push() for the residual async race.
28 *
29 *  3. LAZY-FILL SLOTS. Ad <iframe> creation (triggered by push()) is heavy, so
30 *     visible slots are filled via IntersectionObserver only as they near the
31 *     viewport. A 4 s safety net hides the skeleton shimmer if a slot never
32 *     resolves, so it can't appear to load forever.
33 *
34 * REVERT to eager loading: put the two <script> tags back in <head> of
35 * Master.blade.php / DistancMap.blade.php and delete injectLoaders()/onReady().
36 */
37(function () {
38    'use strict';
39
40    var AD_CLIENT = 'ca-pub-8348016522017879';
41    var loadersInjected = false;
42
43    /**
44     * Inject the third-party ad scripts (PubGuru then AdSense). Guarded so it
45     * only ever runs once per page, no matter how many triggers fire.
46     */
47    function injectLoaders() {
48        if (loadersInjected) return;
49        loadersInjected = true;
50        var head = document.head;
51        var pg = document.createElement('script');
52        pg.async = true;
53        pg.src = '//c.pubguru.net/pghb.onlinecompass_net.tc.js';
54        var ads = document.createElement('script');
55        ads.async = true;
56        ads.crossOrigin = 'anonymous';
57        ads.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=' + AD_CLIENT;
58        head.appendChild(pg);
59        head.appendChild(ads);
60    }
61
62    /**
63     * Run fn at the next idle moment, or on load — whichever comes first.
64     *
65     * NOTE: this previously also fired on the FIRST user interaction
66     * (pointerdown/keydown/touchstart/scroll). That was removed because
67     * injecting the heavy ad libraries *on* the first tap made that very tap
68     * wait for their parse/execute — directly hurting INP (the metric we are
69     * fixing). Visible ad fill is unaffected: it is gated separately by the
70     * IntersectionObserver in init(), so the libraries only need to be present
71     * by the time a slot nears the viewport, which idle (<=2s) + load covers.
72     */
73    function onReady(fn) {
74        var done = false;
75        function run() {
76            if (done) return;
77            done = true;
78            fn();
79        }
80        if ('requestIdleCallback' in window) {
81            window.requestIdleCallback(run, { timeout: 2000 });
82        } else {
83            window.setTimeout(run, 2000);
84        }
85        window.addEventListener('load', run, { once: true });
86    }
87
88    /**
89     * Drop the <ins> from any slot hidden on this viewport. offsetWidth === 0
90     * means the slot (or an ancestor) is display:none; such an <ins> can't be
91     * measured (width 0) and, because push({}) fills ins in document order,
92     * would steal a push meant for a later visible slot. Returns the fillable
93     * (visible) slots.
94     *
95     * This is safe to call immediately at DOMContentLoaded because the
96     * responsive breakpoints (.ad-mobile/.ad-tablet/.ad-desktop visibility)
97     * are inlined in critical-css.blade.php, so hidden variants already have
98     * offsetWidth === 0 from first paint — no need to wait for app.css.
99     */
100    function pruneHidden(slots) {
101        // Read all geometry FIRST, then mutate. Interleaving offsetWidth
102        // reads with ins.remove() writes forces a synchronous layout reflow
103        // on every iteration (layout is invalidated by each removal); batching
104        // the reads collapses them into a single layout calculation.
105        var toRemove = [];
106        var visible = [];
107        for (var i = 0; i < slots.length; i++) {
108            var slot = slots[i];
109            var ins = slot.querySelector('ins.adsbygoogle');
110            if (!ins) continue;
111            if (slot.offsetWidth === 0) {
112                toRemove.push(ins);
113            } else {
114                visible.push(slot);
115            }
116        }
117        for (var j = 0; j < toRemove.length; j++) {
118            toRemove[j].remove();
119        }
120        return visible;
121    }
122
123    /**
124     * Issue one push({}). AdSense's push({}) fills the next <ins> it has not
125     * yet processed (document order) and THROWS "All 'ins' elements ... already
126     * have ads in them" when none is left. PubGuru header-bidding and page-level
127     * auto-ads can fill a slot out-of-band, so a pure count cap can desync
128     * (increment on throw → later slots skip their push). Instead, check for a
129     * live unprocessed <ins> before each push and swallow the residual async-
130     * race TagError (AdSense marks an <ins> processed via data-ad-status,
131     * asynchronously, so there is a window where the guard sees an ins that
132     * AdSense has already claimed internally).
133     */
134    function pushOne() {
135        // push({}) fills the FIRST unprocessed <ins> in document order. If that
136        // <ins> is 0-width (a responsive variant that became hidden after init,
137        // or a layout race after a deploy/cache-purge), AdSense throws
138        // "No slot size for availableWidth=0". Prune it — same as pruneHidden at
139        // init — and skip the push rather than trigger the TagError.
140        var ins = document.querySelector('ins.adsbygoogle:not([data-ad-status])');
141        if (!ins) return;
142        if (ins.offsetWidth === 0) { ins.remove(); return; }
143        try {
144            (window.adsbygoogle = window.adsbygoogle || []).push({});
145        } catch (e) {
146            // TagError: raced past the last unfilled <ins> before AdSense
147            // marked it processed (data-ad-status). Benign — next slot retries.
148        }
149    }
150
151    /** Hide the skeleton shimmer if AdSense never sets data-ad-status. */
152    function watchShimmer(slot) {
153        window.setTimeout(function () {
154            var ins = slot.querySelector('ins.adsbygoogle');
155            if (ins && !ins.getAttribute('data-ad-status')) {
156                slot.classList.add('is-loaded');
157            }
158        }, 4000);
159    }
160
161    function fill(slot) {
162        pushOne();
163        watchShimmer(slot);
164    }
165
166    /**
167     * Run cb once the user has made a Cookiebot consent decision — never before.
168     *
169     * WHY: the ad libraries (PubGuru .tc.js + adsbygoogle.js) pull in Google's
170     * Funding Choices IAB-TCF-v2 consent wall (web_iab_tcf_v2_wall_executable),
171     * a ~0.3 s main-thread long task. Injecting them at idle/load lands that
172     * task in the window where users tap the consent button (0.45 s) or the
173     * compass (0.31 s) — the top INP blockers on the homepage. Holding the ad
174     * libraries until the consent decision keeps the wall out of those
175     * pre-consent interactions entirely (the fc- button doesn't even exist
176     * before consent), and matches what Cookiebot's data-blockingmode="auto"
177     * already does for gtag. This is the GDPR-correct order too: ads.js was
178     * previously able to load AdSense before any consent decision.
179     *
180     * Covers: already-consented returning users (Cookiebot.consent === true),
181     * the live decision (CookiebotOnAccept / CookiebotOnDecline), and a safety
182     * net if Cookiebot is absent/blocked (e.g. a non-EU request with no event).
183     * Slot filling is unaffected: push({}) only queues into the adsbygoogle
184     * array; nothing renders until the library loads here, post-consent.
185     */
186    function whenConsented(cb) {
187        var fired = false;
188        var timer = null;
189        function fire() {
190            if (fired) return;
191            fired = true;
192            window.removeEventListener('CookiebotOnAccept', fire);
193            window.removeEventListener('CookiebotOnDecline', fire);
194            if (timer) clearTimeout(timer);
195            cb();
196        }
197        if (window.Cookiebot && window.Cookiebot.consent) {
198            fire();
199            return;
200        }
201        window.addEventListener('CookiebotOnAccept', fire);
202        window.addEventListener('CookiebotOnDecline', fire);
203        // Safety net: Cookiebot absent / blocked / non-EU without an auto-event.
204        timer = window.setTimeout(fire, 6000);
205    }
206
207    function init() {
208        // Inject ad libraries only AFTER consent, so page-level auto ads still
209        // work on pages with no .ad-slot — but without dropping Google's
210        // Funding Choices wall into the pre-consent interaction window.
211        whenConsented(function () { onReady(injectLoaders); });
212
213        var slots = Array.prototype.slice.call(document.querySelectorAll('.ad-slot'));
214        if (!slots.length) return;
215
216        // Safe to prune immediately: responsive breakpoints live in critical CSS
217        // (critical-css.blade.php), so hidden variants already have
218        // offsetWidth === 0 from first paint — no need to gate on app.css.
219        var visible = pruneHidden(slots);
220        if (!visible.length) return;
221
222        if ('IntersectionObserver' in window) {
223            var io = new IntersectionObserver(function (entries, obs) {
224                for (var i = 0; i < entries.length; i++) {
225                    if (!entries[i].isIntersecting) continue;
226                    obs.unobserve(entries[i].target); // each slot fires at most once
227                    fill(entries[i].target);
228                }
229            /* rootMargin kept tight (200px, down from 600px) so slots fill
230               only just before they enter the viewport. The wider margin
231               filled ads ~1 viewport early, which meant AdSense's
232               show_ads_impl (~95ms main-thread, a top INP LOAF blocker)
233               was already running by the time users tapped/interacted —
234               directly competing with those interactions. 200px still
235               fills before the slot is visible (no blank ad on scroll). */
236            }, { rootMargin: '200px 0px', threshold: 0 });
237            visible.forEach(function (slot) { io.observe(slot); });
238        } else {
239            visible.forEach(function (slot) { fill(slot); });
240        }
241    }
242
243    if (document.readyState === 'loading') {
244        document.addEventListener('DOMContentLoaded', init);
245    } else {
246        init();
247    }
248})();

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.