nodebb-plugin-ezoic-infinite 1.5.70 → 1.5.72

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.5.70",
3
+ "version": "1.5.72",
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
@@ -12,6 +12,10 @@
12
12
  // Smoothness caps
13
13
  const MAX_INSERTS_PER_RUN = 3;
14
14
 
15
+ // Keep empty (unfilled) wraps alive for a while. Topics/messages can fill late (auction/CMP).
16
+ // Pruning too early makes ads look like they "disappear" while scrolling.
17
+ const KEEP_EMPTY_WRAP_MS = 30000;
18
+
15
19
  // Preload margins
16
20
  const PRELOAD_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
17
21
  const PRELOAD_MARGIN_MOBILE = '1500px 0px 1500px 0px';
@@ -207,6 +211,22 @@
207
211
  return el;
208
212
  }
209
213
 
214
+ function primePlaceholderPool(allIds) {
215
+ try {
216
+ if (!Array.isArray(allIds) || !allIds.length) return;
217
+ const pool = getPoolEl();
218
+ for (const id of allIds) {
219
+ if (!id) continue;
220
+ const domId = `${PLACEHOLDER_PREFIX}${id}`;
221
+ if (document.getElementById(domId)) continue;
222
+ const ph = document.createElement('div');
223
+ ph.id = domId;
224
+ ph.setAttribute('data-ezoic-id', String(id));
225
+ pool.appendChild(ph);
226
+ }
227
+ } catch (e) {}
228
+ }
229
+
210
230
  function isInPool(ph) {
211
231
  try { return !!(ph && ph.closest && ph.closest('#' + POOL_ID)); } catch (e) { return false; }
212
232
  }
@@ -319,6 +339,13 @@
319
339
  if (!state.allTopics.length) state.allTopics = parsePool(cfg.placeholderIds);
320
340
  if (!state.allPosts.length) state.allPosts = parsePool(cfg.messagePlaceholderIds);
321
341
  if (!state.allCategories.length) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
342
+
343
+ // Create placeholders up-front in an offscreen pool.
344
+ // Ezoic may attempt to define/show ids during load; if they don't exist yet,
345
+ // it can spam errors and sometimes short-circuit. Pooling keeps ids existing without layout.
346
+ primePlaceholderPool(state.allTopics);
347
+ primePlaceholderPool(state.allPosts);
348
+ primePlaceholderPool(state.allCategories);
322
349
  }
323
350
 
324
351
  // ---------------- insertion primitives ----------------
@@ -328,6 +355,7 @@
328
355
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
329
356
  wrap.setAttribute('data-ezoic-after', String(afterPos));
330
357
  wrap.setAttribute('data-ezoic-wrapid', String(id));
358
+ wrap.setAttribute('data-created', String(now()));
331
359
  wrap.style.width = '100%';
332
360
 
333
361
  if (createPlaceholder) {
@@ -391,43 +419,79 @@
391
419
  const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
392
420
  let removed = 0;
393
421
 
394
- wraps.forEach((wrap) => {
395
- let ok = false;
396
- // NodeBB/skins can insert helper nodes between posts (clearfix, separators, etc.).
397
- // Be tolerant so we don't remove a valid ad too early ("disappears too fast").
422
+ const isFilled = (wrap) => {
423
+ return !!(wrap.querySelector('iframe, ins, img, video, [data-google-container-id]'));
424
+ };
425
+
426
+ const hasNearbyItem = (wrap) => {
427
+ // NodeBB/skins can inject separators/spacers; be tolerant.
398
428
  let prev = wrap.previousElementSibling;
399
- for (let i = 0; i < 8 && prev; i++) {
400
- if (itemSet.has(prev)) { ok = true; break; }
429
+ for (let i = 0; i < 14 && prev; i++) {
430
+ if (itemSet.has(prev)) return true;
401
431
  prev = prev.previousElementSibling;
402
432
  }
403
-
404
- if (!ok) {
405
- let next = wrap.nextElementSibling;
406
- for (let i = 0; i < 3 && next; i++) {
407
- if (itemSet.has(next)) { ok = true; break; }
408
- next = next.nextElementSibling;
409
- }
433
+ let next = wrap.nextElementSibling;
434
+ for (let i = 0; i < 14 && next; i++) {
435
+ if (itemSet.has(next)) return true;
436
+ next = next.nextElementSibling;
410
437
  }
438
+ return false;
439
+ };
411
440
 
412
- if (!ok) {
413
- withInternalDomChange(() => releaseWrapNode(wrap));
414
- removed++;
415
- }
441
+ wraps.forEach((wrap) => {
442
+ if (isFilled(wrap)) return; // never prune filled ads
443
+
444
+ // Never prune a fresh wrap: it may fill late.
445
+ try {
446
+ const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
447
+ if (created && (now() - created) < KEEP_EMPTY_WRAP_MS) return;
448
+ } catch (e) {}
449
+
450
+ if (hasNearbyItem(wrap)) return;
451
+
452
+ withInternalDomChange(() => releaseWrapNode(wrap));
453
+ removed++;
416
454
  });
417
455
 
418
456
  return removed;
419
457
  }
420
458
 
421
459
  function decluster(kindClass) {
422
- // Remove consecutive wraps (keep the first)
460
+ // Remove "near-consecutive" wraps (keep the first). Be tolerant of spacer nodes.
423
461
  const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
424
462
  if (wraps.length < 2) return 0;
463
+
464
+ const isWrap = (el) => !!(el && el.classList && el.classList.contains(WRAP_CLASS));
465
+
466
+ const isFilled = (wrap) => {
467
+ return !!(wrap && wrap.querySelector && wrap.querySelector('iframe, ins, img, video, [data-google-container-id]'));
468
+ };
469
+
470
+ const isFresh = (wrap) => {
471
+ try {
472
+ const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
473
+ return created && (now() - created) < KEEP_EMPTY_WRAP_MS;
474
+ } catch (e) {
475
+ return false;
476
+ }
477
+ };
478
+
425
479
  let removed = 0;
426
480
  for (const w of wraps) {
427
- const prev = w.previousElementSibling;
428
- if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
429
- withInternalDomChange(() => releaseWrapNode(w));
430
- removed++;
481
+ let prev = w.previousElementSibling;
482
+ for (let i = 0; i < 3 && prev; i++) {
483
+ if (isWrap(prev)) {
484
+ // Don't decluster two "fresh" empty wraps — it can reduce fill on slow auctions.
485
+ // Only decluster when at least one is filled, or when the newer one is stale.
486
+ const prevFilled = isFilled(prev);
487
+ const curFilled = isFilled(w);
488
+ if (prevFilled || curFilled || !isFresh(w)) {
489
+ withInternalDomChange(() => releaseWrapNode(w));
490
+ removed++;
491
+ }
492
+ break;
493
+ }
494
+ prev = prev.previousElementSibling;
431
495
  }
432
496
  }
433
497
  return removed;
@@ -479,9 +543,6 @@
479
543
  if (!ph || !ph.isConnected) return;
480
544
  ph.setAttribute('data-ezoic-id', String(id));
481
545
 
482
- // When the ad actually fills, tighten height to the real iframe height and remove empty-collapsing.
483
- armFillObserver(ph);
484
-
485
546
  const io = ensurePreloadObserver();
486
547
  try { io && io.observe(ph); } catch (e) {}
487
548
 
@@ -494,56 +555,6 @@
494
555
  } catch (e) {}
495
556
  }
496
557
 
497
- // ---------------- fill tightening ----------------
498
-
499
- const _fillObserved = new WeakSet();
500
- function tightenFilled(ph) {
501
- if (!ph || !ph.isConnected) return;
502
- const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
503
- if (!wrap) return;
504
-
505
- // If we have any real ad content, we should not be in "empty" mode.
506
- const frame = ph.querySelector ? ph.querySelector('iframe') : null;
507
- const hasAd = !!(frame || (ph.querySelector && ph.querySelector('ins, img, .ez-ad, .ezoic-ad')));
508
- if (!hasAd) return;
509
-
510
- try { wrap.classList.remove('is-empty'); } catch (e) {}
511
-
512
- // Remove the common "extra reserved space" created by Ezoic's min-height styles.
513
- // Set minHeight to the actual iframe height when available.
514
- try {
515
- if (frame && frame.offsetHeight) {
516
- const h = frame.offsetHeight;
517
- const ezoicSpan = ph.closest ? ph.closest('span.ezoic-ad, .ezoic-ad') : null;
518
- if (ezoicSpan && ezoicSpan.style) {
519
- ezoicSpan.style.minHeight = `${h}px`;
520
- }
521
- // Also tighten the wrapper itself to avoid "space under" when the parent is taller.
522
- wrap.style.minHeight = `${h}px`;
523
- }
524
- } catch (e) {}
525
- }
526
-
527
- function armFillObserver(ph) {
528
- if (!ph || _fillObserved.has(ph)) return;
529
- _fillObserved.add(ph);
530
-
531
- // Fast path: already filled
532
- try { tightenFilled(ph); } catch (e) {}
533
-
534
- // Observe until it fills, then disconnect.
535
- try {
536
- const mo = new MutationObserver(() => {
537
- try {
538
- tightenFilled(ph);
539
- const hasFrame = !!(ph.querySelector && ph.querySelector('iframe'));
540
- if (hasFrame) mo.disconnect();
541
- } catch (e) {}
542
- });
543
- mo.observe(ph, { childList: true, subtree: true });
544
- } catch (e) {}
545
- }
546
-
547
558
  function enqueueShow(id) {
548
559
  if (!id || isBlocked()) return;
549
560
 
@@ -588,10 +599,17 @@
588
599
  if (!ph2 || !ph2.isConnected) return;
589
600
  const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
590
601
  if (!w2) return;
602
+
603
+ // Don't collapse "fresh" placements; slow auctions/CMP can fill late.
604
+ try {
605
+ const created = parseInt(w2.getAttribute('data-created') || '0', 10);
606
+ if (created && (now() - created) < KEEP_EMPTY_WRAP_MS) return;
607
+ } catch (e) {}
608
+
591
609
  const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
592
610
  if (!hasAd) w2.classList.add('is-empty');
593
611
  } catch (e) {}
594
- }, 3500);
612
+ }, 15000);
595
613
  } catch (e) {}
596
614
  }
597
615
 
package/public/style.css CHANGED
@@ -22,10 +22,6 @@
22
22
  .nodebb-ezoic-wrap .ezoic-ad {
23
23
  margin: 0 !important;
24
24
  padding: 0 !important;
25
- /* Ezoic sometimes reserves extra height (e.g. min-height: 400px) even when the filled iframe is smaller.
26
- We prefer zero unused space; JS will also tighten after fill to the real iframe height. */
27
- min-height: 0 !important;
28
- height: auto !important;
29
25
  }
30
26
 
31
27
  /* Remove the classic "gap under iframe" (baseline/inline-block) */
@@ -66,3 +62,9 @@
66
62
  margin: 0 !important;
67
63
  padding: 0 !important;
68
64
  }
65
+ /* Remove Ezoic's large reserved min-height inside our wrappers (topics/messages) */
66
+ .nodebb-ezoic-wrap .ezoic-ad,
67
+ .nodebb-ezoic-wrap span.ezoic-ad {
68
+ min-height: 1px !important; /* kill 400px gaps */
69
+ height: auto !important;
70
+ }