nodebb-plugin-ezoic-infinite 1.4.96 → 1.4.97

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.4.96",
3
+ "version": "1.4.97",
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
@@ -7,6 +7,11 @@
7
7
  categoryItem: 'li[component="categories/category"]',
8
8
  }, WRAP_CLASS = 'ezoic-ad';
9
9
  const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-', MAX_INSERTS_PER_RUN = 3;
10
+ // Ultra-fast: preload ads before they enter viewport (especially above-the-fold)
11
+ const PRELOAD_ROOT_MARGIN = '1200px 0px';
12
+ const ABOVE_FOLD_MULT = 1.5; // render immediately if within 1.5× viewport height
13
+ const FAST_START_MS = 2500; // aggressive preload window after page load
14
+
10
15
 
11
16
  // FLAG GLOBAL: Bloquer Ezoic pendant navigation
12
17
  let EZOIC_BLOCKED = false;
@@ -37,7 +42,43 @@
37
42
 
38
43
  obs: null,
39
44
  activeTimeouts: new Set(),
40
- lastScrollRun: 0, };
45
+ lastScrollRun: 0,
46
+ io: null,
47
+ fastStartUntil: 0,
48
+ };
49
+
50
+
51
+ // Track currently inserted ad wrappers for recycling
52
+ const liveArr = [];
53
+
54
+
55
+ // Network warm-up: reduce latency to Ezoic/CDN on first above-the-fold render
56
+ const _warmLinksDone = new Set();
57
+ function warmUpNetwork() {
58
+ try {
59
+ const head = document.head || document.getElementsByTagName('head')[0];
60
+ if (!head) return;
61
+ const links = [
62
+ ['preconnect', 'https://g.ezoic.net', true],
63
+ ['dns-prefetch', 'https://g.ezoic.net', false],
64
+ ['preconnect', 'https://go.ezoic.net', true],
65
+ ['dns-prefetch', 'https://go.ezoic.net', false],
66
+ ['preconnect', 'https://www.ezoic.net', true],
67
+ ['dns-prefetch', 'https://www.ezoic.net', false],
68
+ ];
69
+ for (const [rel, href, cors] of links) {
70
+ const key = rel + '|' + href;
71
+ if (_warmLinksDone.has(key)) continue;
72
+ _warmLinksDone.add(key);
73
+ const link = document.createElement('link');
74
+ link.rel = rel;
75
+ link.href = href;
76
+ if (cors) link.crossOrigin = 'anonymous';
77
+ head.appendChild(link);
78
+ }
79
+ } catch (e) {}
80
+ }
81
+
41
82
 
42
83
  function normalizeBool(v) {
43
84
  return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
@@ -123,9 +164,9 @@
123
164
  } catch (e) {}
124
165
 
125
166
  // Recyclage: libérer IDs après 100ms
126
- setTimeout(() => {
127
- filtered.forEach(id => sessionDefinedIds.delete(id));
128
- }, 100);
167
+ setTimeout(() => {
168
+ filtered.forEach(id => sessionDefinedIds.delete(id));
169
+ }, 100);
129
170
  };
130
171
  try {
131
172
  window.ezstandalone = window.ezstandalone || {};
@@ -197,11 +238,17 @@
197
238
  if (ph && ph.children.length === 0) {
198
239
  // Placeholder vide après 3s = pub non chargée
199
240
  setTimeout(() => {
200
- if (ph.children.length === 0) {
201
- wrapper.remove();
202
- }
203
- }, 1500);
204
- }
241
+ try {
242
+ // Give Ezoic more time on slower connections, and don't remove if we recently requested an ad.
243
+ const phId = ph && ph.id;
244
+ const id = phId ? parseInt(phId.replace(PLACEHOLDER_PREFIX, ''), 10) : 0;
245
+ const last = (id && state.lastShowById && state.lastShowById.get(id)) || 0;
246
+ if (ph.children.length === 0 && (!last || Date.now() - last > 10000)) {
247
+ wrapper.remove();
248
+ }
249
+ } catch (e) {}
250
+ }, 8000);
251
+ }
205
252
  });
206
253
  } catch (e) {}
207
254
  }
@@ -302,67 +349,52 @@
302
349
 
303
350
  function patchShowAds() {
304
351
  const applyPatch = () => {
305
- try {
306
- window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
307
- if (window.__nodebbEzoicPatched) return;
308
- if (typeof ez.showAds !== 'function') return;
309
-
310
- window.__nodebbEzoicPatched = true;
311
- const orig = ez.showAds;
312
-
313
- ez.showAds = function (arg) {
314
- // CRITIQUE: Bloquer TOUS appels si navigation en cours
315
- if (EZOIC_BLOCKED) {
316
- return; // Ignorer complètement
317
- }
318
-
319
- // Filtrer IDs dont placeholders n'existent pas
320
- const filterValidIds = (ids) => {
321
- return ids.filter(id => {
322
- const phId = `ezoic-pub-ad-placeholder-${id}`;
323
- const ph = document.getElementById(phId);
324
- return ph && ph.isConnected;
325
- });
326
- };
327
-
328
- if (Array.isArray(arg)) {
329
- const seen = new Set();
330
- for (const v of arg) {
331
- const id = parseInt(v, 10);
332
- if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
333
-
334
- // Vérifier existence avant appel
335
- const phId = `ezoic-pub-ad-placeholder-${id}`;
336
- const ph = document.getElementById(phId);
337
- if (!ph || !ph.isConnected) continue;
338
-
339
- seen.add(id);
340
- try { orig.call(ez, id); } catch (e) {}
341
- }
342
- return;
343
- }
344
-
345
- // Pour appels simples, vérifier aussi
346
- if (typeof arg === 'number') {
347
- const phId = `ezoic-pub-ad-placeholder-${arg}`;
348
- const ph = document.getElementById(phId);
349
- if (!ph || !ph.isConnected) return;
350
- }
351
-
352
- return orig.apply(ez, arguments);
353
- };
354
- } catch (e) {}
352
+ try {
353
+ window.ezstandalone = window.ezstandalone || {};
354
+ const ez = window.ezstandalone;
355
+ if (window.__nodebbEzoicPatched) return;
356
+ if (typeof ez.showAds !== 'function') return;
357
+
358
+ window.__nodebbEzoicPatched = true;
359
+ const orig = ez.showAds;
360
+
361
+ // Ezoic's ez-standalone.js logs warnings when asked to render an ID
362
+ // that isn't present in the DOM anymore. In an infinite-scroll context
363
+ // we must filter and call per-id.
364
+ ez.showAds = function (...args) {
365
+ if (EZOIC_BLOCKED) return;
366
+
367
+ let ids = [];
368
+ if (args.length === 1 && Array.isArray(args[0])) {
369
+ ids = args[0];
370
+ } else {
371
+ ids = args;
372
+ }
373
+
374
+ const seen = new Set();
375
+ for (const v of ids) {
376
+ const id = parseInt(v, 10);
377
+ if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
378
+
379
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
380
+ if (!ph || !ph.isConnected) continue;
381
+
382
+ seen.add(id);
383
+ try { orig.call(ez, id); } catch (e) {}
384
+ }
385
+ };
386
+ } catch (e) {}
355
387
  };
356
388
 
357
389
  applyPatch();
358
390
  if (!window.__nodebbEzoicPatched) {
359
- try {
360
- window.ezstandalone = window.ezstandalone || {};
361
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
362
- window.ezstandalone.cmd.push(applyPatch);
363
- } catch (e) {}
364
- }
391
+ try {
392
+ window.ezstandalone = window.ezstandalone || {};
393
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
394
+ window.ezstandalone.cmd.push(applyPatch);
395
+ } catch (e) {}
365
396
  }
397
+ }
366
398
 
367
399
  function markFilled(wrap) {
368
400
  try {
@@ -465,7 +497,7 @@
465
497
  window.ezstandalone.cmd.push(function() {
466
498
  if (typeof window.ezstandalone.showAds === 'function') {
467
499
  // Appel batch: showAds(id1, id2, id3...)
468
- window.ezstandalone.showAds(...validIds);
500
+ window.ezstandalone.showAds(validIds);
469
501
  // Tracker tous les IDs
470
502
  validIds.forEach(id => {
471
503
  state.lastShowById.set(id, Date.now());
@@ -482,53 +514,95 @@
482
514
  }, 100);
483
515
  }
484
516
 
485
- function callShowAdsWhenReady(id) {
517
+
518
+ function callShowAdsWhenReady(id) {
486
519
  if (!id) return;
487
520
 
488
521
  const now = Date.now(), last = state.lastShowById.get(id) || 0;
489
- if (now - last < 3500) return;
490
-
491
- const phId = `${PLACEHOLDER_PREFIX}${id}`, doCall = () => {
492
- try {
493
- window.ezstandalone = window.ezstandalone || {};
494
- if (typeof window.ezstandalone.showAds === 'function') {
495
-
496
- state.lastShowById.set(id, Date.now());
497
- window.ezstandalone.showAds(id);
498
- sessionDefinedIds.add(id);
499
- return true;
500
- }
501
- } catch (e) {}
502
- return false;
522
+ if (now - last < 2500) return;
523
+
524
+ const phId = `${PLACEHOLDER_PREFIX}${id}`;
525
+
526
+ const tryShowOrQueue = () => {
527
+ if (EZOIC_BLOCKED) return false;
528
+ const el = document.getElementById(phId);
529
+ if (!el || !el.isConnected) return false;
530
+
531
+ try {
532
+ window.ezstandalone = window.ezstandalone || {};
533
+ const ez = window.ezstandalone;
534
+
535
+ if (typeof ez.showAds === 'function') {
536
+ state.lastShowById.set(id, Date.now());
537
+ ez.showAds(id);
538
+ sessionDefinedIds.add(id);
539
+ return true;
540
+ }
541
+
542
+ // Queue once to run as soon as Ezoic is ready
543
+ ez.cmd = ez.cmd || [];
544
+ if (!el.__ezoicQueued) {
545
+ el.__ezoicQueued = true;
546
+ ez.cmd.push(() => {
547
+ try {
548
+ if (EZOIC_BLOCKED) return;
549
+ const ph = document.getElementById(phId);
550
+ if (!ph || !ph.isConnected) return;
551
+ state.lastShowById.set(id, Date.now());
552
+ window.ezstandalone.showAds(id);
553
+ sessionDefinedIds.add(id);
554
+ } catch (e) {}
555
+ });
556
+ }
557
+ } catch (e) {}
558
+ return false;
503
559
  };
504
560
 
505
561
  const startPageKey = state.pageKey;
506
562
  let attempts = 0;
507
- (function waitForPh() {
508
- if (state.pageKey !== startPageKey) return;
509
- if (state.pendingById.has(id)) return;
510
-
511
- attempts += 1;
512
- const el = document.getElementById(phId);
513
- if (el && el.isConnected) {
514
-
515
- // Si on arrive ici, soit visible, soit timeout
516
563
 
517
- if (doCall()) {
518
- state.pendingById.delete(id);
519
- return;
520
- }
521
-
522
- }
523
-
524
- if (attempts < 100) {
525
- const timeoutId = setTimeout(waitForPh, 50);
526
- state.activeTimeouts.add(timeoutId);
527
- }
564
+ (function waitForPh() {
565
+ if (state.pageKey !== startPageKey) return;
566
+
567
+ const el = document.getElementById(phId);
568
+ if (el && el.isConnected) {
569
+ // tag id for IO callback
570
+ try { el.setAttribute('data-ezoic-id', String(id)); } catch (e) {}
571
+
572
+ // If above-the-fold (or during the fast-start window), fire immediately.
573
+ const inFastWindow = state.fastStartUntil && Date.now() < state.fastStartUntil;
574
+ if (isAboveFold(el) || (inFastWindow && (el.getBoundingClientRect().top < window.innerHeight * 3))) {
575
+ if (tryShowOrQueue()) {
576
+ state.pendingById.delete(id);
577
+ return;
578
+ }
579
+ }
580
+
581
+ // Otherwise, preload via IntersectionObserver
582
+ if (!state.pendingById.has(id)) {
583
+ state.pendingById.add(id);
584
+ const io = ensurePreloadObserver();
585
+ try { io && io.observe(el); } catch (e) {}
586
+
587
+ // Safety fallback: if it still hasn't rendered after 6s, force attempt.
588
+ const t = setTimeout(() => {
589
+ tryShowOrQueue();
590
+ state.pendingById.delete(id);
591
+ }, 6000);
592
+ state.activeTimeouts.add(t);
593
+ }
594
+ return;
595
+ }
596
+
597
+ attempts += 1;
598
+ if (attempts < 100) {
599
+ const timeoutId = setTimeout(waitForPh, 25);
600
+ state.activeTimeouts.add(timeoutId);
601
+ }
528
602
  })();
529
- }
603
+ }
530
604
 
531
- async function fetchConfig() {
605
+ async function fetchConfig() {
532
606
  if (state.cfg) return state.cfg;
533
607
  if (state.cfgPromise) return state.cfgPromise;
534
608
 
@@ -718,6 +792,7 @@
718
792
  state.pendingById.clear();
719
793
 
720
794
  if (state.obs) { state.obs.disconnect(); state.obs = null; }
795
+ if (state.io) { try { state.io.disconnect(); } catch (e) {} state.io = null; }
721
796
 
722
797
  state.scheduled = false;
723
798
  clearTimeout(state.timer);
@@ -730,6 +805,34 @@
730
805
  try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
731
806
  }
732
807
 
808
+ function ensurePreloadObserver() {
809
+ if (state.io) return state.io;
810
+ try {
811
+ state.io = new IntersectionObserver((entries) => {
812
+ for (const ent of entries) {
813
+ if (!ent.isIntersecting) continue;
814
+ const el = ent.target;
815
+ try { state.io && state.io.unobserve(el); } catch (e) {}
816
+ const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
817
+ const id = parseInt(idAttr, 10);
818
+ if (Number.isFinite(id) && id > 0) {
819
+ // Try immediately; otherwise it will queue via ezstandalone.cmd
820
+ try { callShowAdsWhenReady(id); } catch (e) {}
821
+ }
822
+ }
823
+ }, { root: null, rootMargin: PRELOAD_ROOT_MARGIN, threshold: 0.01 });
824
+ } catch (e) { state.io = null; }
825
+ return state.io;
826
+ }
827
+
828
+ function isAboveFold(el) {
829
+ try {
830
+ if (!el || !el.getBoundingClientRect) return false;
831
+ const r = el.getBoundingClientRect();
832
+ return r.top < (window.innerHeight * ABOVE_FOLD_MULT) && r.bottom > -200;
833
+ } catch (e) { return false; }
834
+ }
835
+
733
836
  async function runCore() {
734
837
  // CRITIQUE: Ne rien insérer si navigation en cours
735
838
  if (EZOIC_BLOCKED) {
@@ -821,17 +924,28 @@
821
924
 
822
925
  $(window).on('action:ajaxify.start.ezoicInfinite', () => cleanup());
823
926
 
824
- $(window).on('action:ajaxify.end.ezoicInfinite', () => {
927
+ $(window).on('action:ajaxify.end.ezoicInfinite', () => {
825
928
  state.pageKey = getPageKey();
826
-
929
+
827
930
  // Débloquer Ezoic IMMÉDIATEMENT pour la nouvelle page
828
931
  EZOIC_BLOCKED = false;
829
-
932
+
933
+ // Warm-up réseau + runtime Ezoic dès que possible
934
+ warmUpNetwork();
935
+ patchShowAds();
936
+
937
+ // Ultra-fast preload window
938
+ state.fastStartUntil = Date.now() + FAST_START_MS;
939
+
830
940
  ensureObserver();
941
+ ensurePreloadObserver();
831
942
 
832
943
  state.canShowAds = true;
833
-
834
- // CRITIQUE: Relancer insertion maintenant que navigation est terminée
944
+
945
+ // HERO slot ASAP (above-the-fold)
946
+ insertHeroAdEarly();
947
+
948
+ // Relancer l'insertion normale
835
949
  scheduleRun();
836
950
  });
837
951
 
@@ -933,8 +1047,16 @@
933
1047
  bind();
934
1048
  bindScroll();
935
1049
  ensureObserver();
1050
+ ensurePreloadObserver();
936
1051
  state.pageKey = getPageKey();
937
1052
 
1053
+ // Warm-up réseau dès le boot
1054
+ warmUpNetwork();
1055
+ patchShowAds();
1056
+ state.fastStartUntil = Date.now() + FAST_START_MS;
1057
+ // HERO slot ASAP au premier chargement
1058
+ insertHeroAdEarly();
1059
+
938
1060
  // Attendre que Ezoic soit chargé avant d'insérer
939
1061
  waitForEzoicThenRun();
940
1062
  })();
package/public/style.css CHANGED
@@ -4,4 +4,16 @@ span.ezoic-ad,
4
4
  span[class*="ezoic"] {
5
5
  min-height: 0 !important;
6
6
  min-width: 0 !important;
7
- }
7
+ }
8
+ /* Reduce layout shifts and kill extra spacing around Ezoic wrappers */
9
+ .ezoic-ad {
10
+ display: block;
11
+ width: 100%;
12
+ margin: 0 !important;
13
+ padding: 0 !important;
14
+ overflow: hidden;
15
+ }
16
+ .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
17
+ margin: 0 !important;
18
+ padding: 0 !important;
19
+ }