nodebb-plugin-ezoic-infinite 1.8.65 → 1.8.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/library.js CHANGED
@@ -87,11 +87,8 @@ async function isUserExcluded(uid, excludedGroups) {
87
87
  const value = (userGroups[0] || []).some(g => excludedSet.has(g.name));
88
88
 
89
89
  _excludeCache.set(key, { value, at: Date.now() });
90
-
91
- // Limit cache size to prevent unbounded growth
92
90
  if (_excludeCache.size > 1000) {
93
- const oldest = _excludeCache.keys().next().value;
94
- _excludeCache.delete(oldest);
91
+ _excludeCache.delete(_excludeCache.keys().next().value);
95
92
  }
96
93
 
97
94
  return value;
@@ -124,7 +121,7 @@ function buildClientConfig(settings, excluded) {
124
121
  }
125
122
 
126
123
  function serializeInlineConfig(cfg) {
127
- return `<script>window.__nbbEzoicCfg=${JSON.stringify(cfg).replace(/</g, '\\u003c')};</script>`;
124
+ return `<script data-cfasync="false">window.__nbbEzoicCfg=${JSON.stringify(cfg).replace(/</g, '\\u003c')};</script>`;
128
125
  }
129
126
 
130
127
  // ── Head injection ───────────────────────────────────────────────────────────
@@ -138,14 +135,13 @@ const HEAD_PRECONNECTS = [
138
135
  '<link rel="dns-prefetch" href="https://securepubads.g.doubleclick.net">',
139
136
  ].join('\n');
140
137
 
138
+ // Match v50 exactly: CMP → sa.min.js, no stubs.
139
+ // sa.min.js defines _ezaq and ezstandalone itself.
140
+ // Placing stubs before or alongside sa.min.js changes Ezoic's init path
141
+ // and causes "Timed out waiting for loadingStatus" errors.
141
142
  const EZOIC_SCRIPTS = [
142
143
  '<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>',
143
144
  '<script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>',
144
- '<script>',
145
- 'window._ezaq = window._ezaq || {};',
146
- 'window.ezstandalone = window.ezstandalone || {};',
147
- 'ezstandalone.cmd = ezstandalone.cmd || [];',
148
- '</script>',
149
145
  '<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>',
150
146
  '<script src="//ezoicanalytics.com/analytics.js"></script>',
151
147
  ].join('\n');
@@ -166,10 +162,6 @@ plugin.addAdminNavigation = async (header) => {
166
162
  return header;
167
163
  };
168
164
 
169
- /**
170
- * Inject Ezoic scripts into <head> via templateData.customHTML.
171
- * NodeBB v4/Harmony: header.tpl has {{customHTML}} in <head>.
172
- */
173
165
  plugin.injectEzoicHead = async (data) => {
174
166
  try {
175
167
  const settings = await getSettings();
@@ -177,17 +169,14 @@ plugin.injectEzoicHead = async (data) => {
177
169
  const excluded = await isUserExcluded(uid, settings.excludedGroups);
178
170
 
179
171
  if (excluded) {
180
- // Even for excluded users, inject a minimal stub so that any script
181
- // referencing ezstandalone doesn't throw a ReferenceError, plus
182
- // the config with excluded=true so client.js bails early.
183
172
  const cfg = buildClientConfig(settings, true);
184
- const stub = [
185
- '<script>',
186
- 'window._ezaq = window._ezaq || {};',
187
- 'window.ezstandalone = window.ezstandalone || {};',
188
- 'ezstandalone.cmd = ezstandalone.cmd || [];',
189
- '</script>',
190
- ].join('\n');
173
+ // Minimal stub for excluded users — prevents ReferenceError if other
174
+ // scripts reference _ezaq or ezstandalone
175
+ const stub = '<script>'
176
+ + 'window._ezaq=window._ezaq||{};'
177
+ + 'window.ezstandalone=window.ezstandalone||{};'
178
+ + 'window.ezstandalone.cmd=window.ezstandalone.cmd||[];'
179
+ + '</script>';
191
180
  data.templateData.customHTML =
192
181
  stub + '\n' +
193
182
  serializeInlineConfig(cfg) +
@@ -235,4 +224,4 @@ plugin.init = async ({ router, middleware }) => {
235
224
  });
236
225
  };
237
226
 
238
- module.exports = plugin;
227
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.65",
3
+ "version": "1.8.67",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -1,12 +1,11 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v2.3.0
2
+ * NodeBB Ezoic Infinite Ads — client.js v2.4.0
3
3
  *
4
- * Architecture based on battle-tested v50, with targeted improvements:
5
- * - Ezoic API: showAds() + destroyPlaceholders() per official docs
6
- * - wrapsByClass Set for O(1) recycle lookup (no querySelectorAll)
7
- * - MutationObserver: ad fill detection + virtualization re-observe
8
- * - Conservative empty check (30s/60s, GPT-aware)
9
- * - aria-hidden + TCF locator protection
4
+ * Architecture: proven v50 core + targeted improvements.
5
+ * Ezoic API: showAds() + destroyPlaceholders() per official docs.
6
+ * Key features: O(1) recycle via wrapsByClass, MutationObserver fill detect,
7
+ * conservative empty check, aria-hidden + TCF protection, retry boot for
8
+ * Cloudflare/async timing.
10
9
  */
11
10
  (function nbbEzoicInfinite() {
12
11
  'use strict';
@@ -77,35 +76,28 @@
77
76
  // ── State ──────────────────────────────────────────────────────────────────
78
77
 
79
78
  const S = {
80
- pageKey: null,
81
- kind: null,
82
- cfg: null,
83
-
84
- poolsReady: false,
85
- pools: { topics: [], posts: [], categories: [] },
86
- cursors: { topics: 0, posts: 0, categories: 0 },
87
-
79
+ pageKey: null,
80
+ kind: null,
81
+ cfg: null,
82
+ poolsReady: false,
83
+ pools: { topics: [], posts: [], categories: [] },
84
+ cursors: { topics: 0, posts: 0, categories: 0 },
88
85
  mountedIds: new Set(),
89
86
  lastShow: new Map(),
90
-
91
- wrapByKey: new Map(), // anchorKey → wrap element
92
- wrapsByClass: new Map(), // kindClass → Set<wrap>
93
-
94
- io: null,
95
- domObs: null,
96
-
97
- mutGuard: 0,
98
- blockedUntil: 0,
99
-
100
- inflight: 0,
101
- pending: [],
102
- pendingSet: new Set(),
103
-
104
- runQueued: false,
105
- burstActive: false,
87
+ wrapByKey: new Map(),
88
+ wrapsByClass: new Map(),
89
+ io: null,
90
+ domObs: null,
91
+ mutGuard: 0,
92
+ blockedUntil: 0,
93
+ inflight: 0,
94
+ pending: [],
95
+ pendingSet: new Set(),
96
+ runQueued: false,
97
+ burstActive: false,
106
98
  burstDeadline: 0,
107
- burstCount: 0,
108
- lastBurstTs: 0,
99
+ burstCount: 0,
100
+ lastBurstTs: 0,
109
101
  };
110
102
 
111
103
  const isBlocked = () => now() < S.blockedUntil;
@@ -214,15 +206,13 @@
214
206
  }
215
207
 
216
208
  // ── GC disconnected wraps ──────────────────────────────────────────────────
217
- // NodeBB virtualizes posts off-viewport. The MutationObserver catches most
218
- // removals, but this is a safety net for edge cases.
219
209
 
220
210
  function gcDisconnectedWraps() {
221
- for (const [key, w] of S.wrapByKey) {
211
+ for (const [key, w] of Array.from(S.wrapByKey)) {
222
212
  if (!w?.isConnected) S.wrapByKey.delete(key);
223
213
  }
224
- for (const [klass, set] of S.wrapsByClass) {
225
- for (const w of set) {
214
+ for (const [klass, set] of Array.from(S.wrapsByClass)) {
215
+ for (const w of Array.from(set)) {
226
216
  if (w?.isConnected) continue;
227
217
  set.delete(w);
228
218
  const id = parseInt(w.getAttribute?.(ATTR.WRAPID), 10);
@@ -261,9 +251,7 @@
261
251
  } catch (_) { return false; }
262
252
  }
263
253
 
264
- function adjacentWrap(el) {
265
- return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
266
- }
254
+ const adjacentWrap = el => wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
267
255
 
268
256
  // ── Fill detection ─────────────────────────────────────────────────────────
269
257
 
@@ -297,15 +285,13 @@
297
285
  }
298
286
 
299
287
  // ── Recycling ──────────────────────────────────────────────────────────────
300
- // Per Ezoic docs: destroyPlaceholders(id) → remove HTML → fresh placeholder → showAds(id)
301
288
 
302
289
  function recycleWrap(klass, targetEl, newKey) {
303
290
  const ez = window.ezstandalone;
304
291
  if (typeof ez?.destroyPlaceholders !== 'function' ||
305
292
  typeof ez?.showAds !== 'function') return null;
306
293
 
307
- const vh = window.innerHeight || 800;
308
- const threshold = -(3 * vh);
294
+ const threshold = -(3 * (window.innerHeight || 800));
309
295
  const t = now();
310
296
  let bestEmpty = null, bestEmptyY = Infinity;
311
297
  let bestFull = null, bestFullY = Infinity;
@@ -329,21 +315,17 @@
329
315
 
330
316
  const best = bestEmpty ?? bestFull;
331
317
  if (!best) return null;
332
-
333
318
  const id = parseInt(best.getAttribute(ATTR.WRAPID), 10);
334
319
  if (!Number.isFinite(id)) return null;
335
320
  const oldKey = best.getAttribute(ATTR.ANCHOR);
336
321
 
337
- // Unobserve before moving
338
322
  try {
339
323
  const ph = best.querySelector(`#${PH_PREFIX}${id}`);
340
324
  if (ph) S.io?.unobserve(ph);
341
325
  } catch (_) {}
342
326
 
343
- // Ezoic recycle: destroy → fresh DOM → showAds
344
327
  const doRecycle = () => {
345
328
  try { ez.destroyPlaceholders(id); } catch (_) {}
346
-
347
329
  setTimeout(() => {
348
330
  mutate(() => {
349
331
  best.setAttribute(ATTR.ANCHOR, newKey);
@@ -351,21 +333,15 @@
351
333
  best.setAttribute(ATTR.SHOWN, '0');
352
334
  best.classList.remove('is-empty');
353
335
  best.replaceChildren();
354
-
355
336
  const fresh = document.createElement('div');
356
337
  fresh.id = `${PH_PREFIX}${id}`;
357
338
  fresh.setAttribute('data-ezoic-id', String(id));
358
339
  best.appendChild(fresh);
359
340
  targetEl.insertAdjacentElement('afterend', best);
360
341
  });
361
-
362
342
  if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
363
343
  S.wrapByKey.set(newKey, best);
364
-
365
- setTimeout(() => {
366
- observePh(id);
367
- enqueueShow(id);
368
- }, TIMING.RECYCLE_DELAY_MS);
344
+ setTimeout(() => { observePh(id); enqueueShow(id); }, TIMING.RECYCLE_DELAY_MS);
369
345
  }, TIMING.RECYCLE_DELAY_MS);
370
346
  };
371
347
 
@@ -431,14 +407,13 @@
431
407
  const cfg = KIND[klass];
432
408
  const wraps = S.wrapsByClass.get(klass);
433
409
  if (!wraps?.size) return;
434
-
435
410
  const liveAnchors = new Set();
436
411
  for (const el of document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`)) {
437
412
  const v = el.getAttribute(cfg.anchorAttr);
438
413
  if (v) liveAnchors.add(v);
439
414
  }
440
415
  const t = now();
441
- for (const w of wraps) {
416
+ for (const w of Array.from(wraps)) {
442
417
  const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
443
418
  if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
444
419
  const key = w.getAttribute(ATTR.ANCHOR) ?? '';
@@ -480,8 +455,7 @@
480
455
  const w = insertAfter(el, id, klass, key);
481
456
  if (w) { observePh(id); inserted++; }
482
457
  } else {
483
- const recycled = recycleWrap(klass, el, key);
484
- if (!recycled) break;
458
+ if (!recycleWrap(klass, el, key)) break;
485
459
  inserted++;
486
460
  }
487
461
  }
@@ -552,11 +526,9 @@
552
526
  if (isBlocked()) { clearTimeout(timer); return release(); }
553
527
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
554
528
  if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
555
-
556
529
  const t = now();
557
530
  if (t - (S.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
558
531
  S.lastShow.set(id, t);
559
-
560
532
  const wrap = ph.closest(`.${WRAP_CLASS}`);
561
533
  try { wrap?.setAttribute(ATTR.SHOWN, String(t)); } catch (_) {}
562
534
 
@@ -582,7 +554,6 @@
582
554
  if (!wrap || !ph?.isConnected) return;
583
555
  if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
584
556
  if (clearEmptyIfFilled(wrap)) return;
585
- // Don't collapse if GPT slot exists (still loading)
586
557
  if (ph.querySelector('[id^="div-gpt-ad"]')) return;
587
558
  if (ph.offsetHeight > 10) return;
588
559
  wrap.classList.add('is-empty');
@@ -592,7 +563,6 @@
592
563
  }
593
564
 
594
565
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
595
- // Matches v50: individual calls, no batching, pass-through no-arg calls.
596
566
 
597
567
  function patchShowAds() {
598
568
  const apply = () => {
@@ -603,7 +573,7 @@
603
573
  window.__nbbEzPatched = true;
604
574
  const orig = ez.showAds.bind(ez);
605
575
  ez.showAds = function (...args) {
606
- if (args.length === 0) return orig();
576
+ if (!args.length) return orig();
607
577
  if (isBlocked()) return;
608
578
  const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
609
579
  const seen = new Set();
@@ -640,24 +610,19 @@
640
610
 
641
611
  const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
642
612
  if (!normBool(cfgEnable)) return 0;
643
- const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
644
- return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
613
+ return injectBetween(klass, getItems(), Math.max(1, parseInt(cfgInterval, 10) || 3), normBool(cfgShowFirst), poolKey);
645
614
  };
646
615
 
647
- if (kind === 'topic') {
648
- return exec('ezoic-ad-message', getPosts,
649
- cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
650
- }
616
+ if (kind === 'topic')
617
+ return exec('ezoic-ad-message', getPosts, cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
651
618
  if (kind === 'categoryTopics') {
652
619
  pruneOrphansBetween();
653
- return exec('ezoic-ad-between', getTopics,
654
- cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
620
+ return exec('ezoic-ad-between', getTopics, cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
655
621
  }
656
- return exec('ezoic-ad-categories', getCategories,
657
- cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
622
+ return exec('ezoic-ad-categories', getCategories, cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
658
623
  }
659
624
 
660
- // ── Scheduler & burst ──────────────────────────────────────────────────────
625
+ // ── Scheduler ──────────────────────────────────────────────────────────────
661
626
 
662
627
  function scheduleRun(cb) {
663
628
  if (S.runQueued) return;
@@ -694,7 +659,7 @@
694
659
  step();
695
660
  }
696
661
 
697
- // ── Cleanup on navigation ──────────────────────────────────────────────────
662
+ // ── Cleanup ────────────────────────────────────────────────────────────────
698
663
 
699
664
  function cleanup() {
700
665
  S.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
@@ -730,53 +695,37 @@
730
695
  kind === 'categoryTopics' ? [SEL.topic] :
731
696
  kind === 'categories' ? [SEL.category] :
732
697
  [SEL.post, SEL.topic, SEL.category];
698
+ outer:
733
699
  for (const m of muts) {
734
700
  if (m.type !== 'childList') continue;
735
- // Free IDs from wraps removed by NodeBB virtualization
736
701
  for (const node of m.removedNodes) {
737
702
  if (!(node instanceof Element)) continue;
738
703
  try {
739
- if (node.classList?.contains(WRAP_CLASS)) {
740
- dropWrap(node);
741
- } else {
742
- const wraps = node.querySelectorAll?.(`.${WRAP_CLASS}`);
743
- if (wraps?.length) for (const w of wraps) dropWrap(w);
744
- }
704
+ if (node.classList?.contains(WRAP_CLASS)) dropWrap(node);
705
+ else { const ws = node.querySelectorAll?.(`.${WRAP_CLASS}`); if (ws?.length) for (const w of ws) dropWrap(w); }
745
706
  } catch (_) {}
746
707
  }
747
708
  for (const node of m.addedNodes) {
748
709
  if (!(node instanceof Element)) continue;
749
- // Ad fill detection → uncollapse
750
710
  try {
751
711
  if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
752
- const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ||
753
- m.target?.closest?.(`.${WRAP_CLASS}.is-empty`);
712
+ const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) || m.target?.closest?.(`.${WRAP_CLASS}.is-empty`);
754
713
  if (wrap) clearEmptyIfFilled(wrap);
755
714
  }
756
715
  } catch (_) {}
757
- // Re-observe wraps re-inserted by NodeBB virtualization
758
716
  try {
759
- const reinserted = node.classList?.contains(WRAP_CLASS)
760
- ? [node]
761
- : Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
717
+ const reinserted = node.classList?.contains(WRAP_CLASS) ? [node] : Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
762
718
  for (const wrap of reinserted) {
763
719
  const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
764
- if (id > 0) {
765
- const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
766
- if (ph) try { getIO()?.observe(ph); } catch (_) {}
767
- }
720
+ if (id > 0) { const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`); if (ph) try { getIO()?.observe(ph); } catch (_) {} }
768
721
  }
769
722
  } catch (_) {}
770
- // New content detection
771
723
  if (!needsBurst) {
772
724
  for (const sel of relevantSels) {
773
- try {
774
- if (node.matches(sel) || node.querySelector(sel)) { needsBurst = true; break; }
775
- } catch (_) {}
725
+ try { if (node.matches(sel) || node.querySelector(sel)) { needsBurst = true; break outer; } } catch (_) {}
776
726
  }
777
727
  }
778
728
  }
779
- if (needsBurst) break;
780
729
  }
781
730
  if (needsBurst) requestBurst();
782
731
  });
@@ -789,65 +738,34 @@
789
738
  if (!window.__tcfapi && !window.__cmp) return;
790
739
  const LOCATOR_ID = '__tcfapiLocator';
791
740
  const ensureInHead = () => {
792
- let existing = document.getElementById(LOCATOR_ID);
793
- if (existing) {
794
- if (existing.parentElement !== document.head) {
795
- try { document.head.appendChild(existing); } catch (_) {}
796
- }
797
- return;
798
- }
741
+ let el = document.getElementById(LOCATOR_ID);
742
+ if (el) { if (el.parentElement !== document.head) try { document.head.appendChild(el); } catch (_) {} return; }
799
743
  const f = document.createElement('iframe');
800
- f.style.display = 'none';
801
- f.id = f.name = LOCATOR_ID;
802
- try { document.head.appendChild(f); } catch (_) {
803
- (document.body || document.documentElement).appendChild(f);
804
- }
744
+ f.style.display = 'none'; f.id = f.name = LOCATOR_ID;
745
+ try { document.head.appendChild(f); } catch (_) { (document.body || document.documentElement).appendChild(f); }
805
746
  };
806
747
  ensureInHead();
807
-
808
748
  if (!window.__nbbCmpGuarded) {
809
749
  window.__nbbCmpGuarded = true;
810
750
  if (typeof window.__tcfapi === 'function') {
811
- const origTcf = window.__tcfapi;
812
- window.__tcfapi = function (cmd, version, cb, param) {
813
- try {
814
- return origTcf.call(this, cmd, version, function (...args) {
815
- try { cb?.(...args); } catch (_) {}
816
- }, param);
817
- } catch (e) {
818
- if (e?.message?.includes('null')) {
819
- ensureInHead();
820
- try { return origTcf.call(this, cmd, version, cb, param); } catch (_) {}
821
- }
822
- }
751
+ const orig = window.__tcfapi;
752
+ window.__tcfapi = function (cmd, ver, cb, param) {
753
+ try { return orig.call(this, cmd, ver, function (...a) { try { cb?.(...a); } catch (_) {} }, param); }
754
+ catch (e) { if (e?.message?.includes('null')) { ensureInHead(); try { return orig.call(this, cmd, ver, cb, param); } catch (_) {} } }
823
755
  };
824
756
  }
825
757
  if (typeof window.__cmp === 'function') {
826
- const origCmp = window.__cmp;
827
- window.__cmp = function (...args) {
828
- try { return origCmp.apply(this, args); }
829
- catch (e) {
830
- if (e?.message?.includes('null')) {
831
- ensureInHead();
832
- try { return origCmp.apply(this, args); } catch (_) {}
833
- }
834
- }
758
+ const orig = window.__cmp;
759
+ window.__cmp = function (...a) {
760
+ try { return orig.apply(this, a); }
761
+ catch (e) { if (e?.message?.includes('null')) { ensureInHead(); try { return orig.apply(this, a); } catch (_) {} } }
835
762
  };
836
763
  }
837
764
  }
838
-
839
765
  if (!window.__nbbTcfObs) {
840
- window.__nbbTcfObs = new MutationObserver(() => {
841
- if (!document.getElementById(LOCATOR_ID)) ensureInHead();
842
- });
843
- try {
844
- window.__nbbTcfObs.observe(document.body || document.documentElement, {
845
- childList: true, subtree: false,
846
- });
847
- if (document.head) {
848
- window.__nbbTcfObs.observe(document.head, { childList: true, subtree: false });
849
- }
850
- } catch (_) {}
766
+ window.__nbbTcfObs = new MutationObserver(() => { if (!document.getElementById(LOCATOR_ID)) ensureInHead(); });
767
+ try { window.__nbbTcfObs.observe(document.body || document.documentElement, { childList: true, subtree: false }); } catch (_) {}
768
+ try { if (document.head) window.__nbbTcfObs.observe(document.head, { childList: true, subtree: false }); } catch (_) {}
851
769
  }
852
770
  }
853
771
 
@@ -855,20 +773,10 @@
855
773
 
856
774
  function protectAriaHidden() {
857
775
  if (window.__nbbAriaObs) return;
858
- const remove = () => {
859
- try {
860
- if (document.body.getAttribute('aria-hidden') === 'true') {
861
- document.body.removeAttribute('aria-hidden');
862
- }
863
- } catch (_) {}
864
- };
865
- remove();
866
- window.__nbbAriaObs = new MutationObserver(remove);
867
- try {
868
- window.__nbbAriaObs.observe(document.body, {
869
- attributes: true, attributeFilter: ['aria-hidden'],
870
- });
871
- } catch (_) {}
776
+ const fix = () => { try { if (document.body.getAttribute('aria-hidden') === 'true') document.body.removeAttribute('aria-hidden'); } catch (_) {} };
777
+ fix();
778
+ window.__nbbAriaObs = new MutationObserver(fix);
779
+ try { window.__nbbAriaObs.observe(document.body, { attributes: true, attributeFilter: ['aria-hidden'] }); } catch (_) {}
872
780
  }
873
781
 
874
782
  // ── Console muting ─────────────────────────────────────────────────────────
@@ -877,31 +785,26 @@
877
785
  if (window.__nbbEzMuted) return;
878
786
  window.__nbbEzMuted = true;
879
787
  const MUTED = [
880
- '[EzoicAds JS]: Placeholder Id',
881
- 'No valid placeholders for loadMore',
882
- 'cannot call refresh on the same page',
883
- 'no placeholders are currently defined in Refresh',
884
- 'Debugger iframe already exists',
885
- '[CMP] Error in custom getTCData',
886
- 'vignette: no interstitial API',
887
- 'Ezoic JS-Enable should only ever',
888
- '### sending slotDestroyed',
889
- 'Error loading identity bridging',
890
788
  `with id ${PH_PREFIX}`,
891
- 'adsbygoogle.push() error: All',
892
- 'has already been defined',
789
+ 'adsbygoogle.push() error',
790
+ 'already been defined',
893
791
  'bad response. Status',
894
- 'slotDestroyed event',
792
+ 'slotDestroyed',
895
793
  'identity bridging',
794
+ '[EzoicAds JS]: Placeholder',
795
+ 'No valid placeholders',
796
+ 'cannot call refresh',
797
+ 'no placeholders are currently defined',
798
+ 'Debugger iframe already',
799
+ 'Error in custom getTCData',
800
+ 'no interstitial API',
801
+ 'JS-Enable should only',
896
802
  ];
897
803
  for (const method of ['log', 'info', 'warn', 'error']) {
898
804
  const orig = console[method];
899
805
  if (typeof orig !== 'function') continue;
900
806
  console[method] = function (...args) {
901
- if (typeof args[0] === 'string') {
902
- const msg = args[0];
903
- for (const p of MUTED) { if (msg.includes(p)) return; }
904
- }
807
+ if (typeof args[0] === 'string') { for (const p of MUTED) if (args[0].includes(p)) return; }
905
808
  return orig.apply(console, args);
906
809
  };
907
810
  }
@@ -909,8 +812,10 @@
909
812
 
910
813
  // ── Network warmup ─────────────────────────────────────────────────────────
911
814
 
912
- const _warmed = new Set();
815
+ let _warmed = false;
913
816
  function warmNetwork() {
817
+ if (_warmed) return;
818
+ _warmed = true;
914
819
  const head = document.head;
915
820
  if (!head) return;
916
821
  for (const [rel, href, cors] of [
@@ -921,9 +826,7 @@
921
826
  ['dns-prefetch', 'https://g.ezoic.net', false],
922
827
  ['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
923
828
  ]) {
924
- const k = `${rel}|${href}`;
925
- if (_warmed.has(k)) continue;
926
- _warmed.add(k);
829
+ if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
927
830
  const l = document.createElement('link');
928
831
  l.rel = rel; l.href = href;
929
832
  if (cors) l.crossOrigin = 'anonymous';
@@ -939,30 +842,22 @@
939
842
  $(window).off('.nbbEzoic');
940
843
  $(window).on('action:ajaxify.start.nbbEzoic', cleanup);
941
844
  $(window).on('action:ajaxify.end.nbbEzoic', () => {
942
- S.pageKey = pageKey();
943
- S.kind = null;
944
- S.blockedUntil = 0;
845
+ S.pageKey = pageKey(); S.kind = null; S.blockedUntil = 0;
945
846
  muteConsole(); ensureTcfLocator(); protectAriaHidden();
946
847
  warmNetwork(); patchShowAds(); getIO(); ensureDomObserver();
947
848
  requestBurst();
948
849
  });
949
-
950
- const burstEvents = [
850
+ const burstEvts = [
951
851
  'action:ajaxify.contentLoaded', 'action:posts.loaded',
952
852
  'action:topics.loaded', 'action:categories.loaded',
953
853
  'action:category.loaded', 'action:topic.loaded',
954
854
  ].map(e => `${e}.nbbEzoic`).join(' ');
955
- $(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
956
-
855
+ $(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
957
856
  try {
958
857
  require(['hooks'], hooks => {
959
858
  if (typeof hooks?.on !== 'function') return;
960
- for (const ev of [
961
- 'action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
962
- 'action:categories.loaded', 'action:topic.loaded',
963
- ]) {
859
+ for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded', 'action:categories.loaded', 'action:topic.loaded'])
964
860
  try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
965
- }
966
861
  });
967
862
  } catch (_) {}
968
863
  }
@@ -970,8 +865,7 @@
970
865
  function bindScroll() {
971
866
  let ticking = false;
972
867
  window.addEventListener('scroll', () => {
973
- if (ticking) return;
974
- ticking = true;
868
+ if (ticking) return; ticking = true;
975
869
  requestAnimationFrame(() => { ticking = false; requestBurst(); });
976
870
  }, { passive: true });
977
871
  }
@@ -991,4 +885,20 @@
991
885
  S.blockedUntil = 0;
992
886
  requestBurst();
993
887
 
888
+ // Retry boot: sa.min.js async + Cloudflare Rocket Loader + NodeBB SPA
889
+ // can cause client.js to boot before DOM/Ezoic are ready.
890
+ // Retries stop once ads are mounted or after ~10s.
891
+ let _retries = 0;
892
+ function retryBoot() {
893
+ if (_retries >= 12 || S.mountedIds.size > 0) return;
894
+ _retries++;
895
+ patchShowAds();
896
+ if (!isBlocked() && !S.burstActive) {
897
+ S.lastBurstTs = 0;
898
+ requestBurst();
899
+ }
900
+ setTimeout(retryBoot, _retries <= 4 ? 300 : 1000);
901
+ }
902
+ setTimeout(retryBoot, 250);
903
+
994
904
  })();
package/public/style.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * NodeBB Ezoic Infinite Ads — style.css v2.0
2
+ * NodeBB Ezoic Infinite Ads — style.css v2.4
3
3
  */
4
4
 
5
5
  /* ── Wrapper ──────────────────────────────────────────────────────────────── */
@@ -112,10 +112,6 @@
112
112
 
113
113
  /* ── Mobile ───────────────────────────────────────────────────────────────── */
114
114
  @media (max-width: 767px) {
115
- html, body {
116
- overflow-x: hidden !important;
117
- }
118
-
119
115
  .nodebb-ezoic-wrap,
120
116
  .nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"],
121
117
  .nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] > div {
@@ -149,7 +145,6 @@
149
145
 
150
146
  .nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
151
147
  overflow-x: visible !important;
152
- -webkit-overflow-scrolling: auto;
153
148
  }
154
149
  }
155
150