nodebb-plugin-ezoic-infinite 1.5.49 → 1.5.50

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.49",
3
+ "version": "1.5.50",
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
@@ -96,6 +96,48 @@
96
96
 
97
97
  const insertingIds = new Set();
98
98
 
99
+ // ---------- lightweight "fade-in" for ad iframes ----------
100
+ // This reduces the perception of "flashing" when a slot appears empty then fills.
101
+ // We avoid scroll listeners and only react to DOM insertions.
102
+ const _faded = new WeakSet();
103
+ function _fadeInIframe(iframe) {
104
+ try {
105
+ if (!iframe || _faded.has(iframe)) return;
106
+ _faded.add(iframe);
107
+ iframe.style.opacity = '0';
108
+ iframe.style.transition = 'opacity 140ms ease';
109
+ // Next frame: show
110
+ requestAnimationFrame(() => {
111
+ try { iframe.style.opacity = '1'; } catch (e) {}
112
+ });
113
+ } catch (e) {}
114
+ }
115
+
116
+ const _adFillObserver = new MutationObserver((muts) => {
117
+ try {
118
+ for (const m of muts) {
119
+ if (m.addedNodes && m.addedNodes.length) {
120
+ for (const n of m.addedNodes) {
121
+ if (!n || n.nodeType !== 1) continue;
122
+ if (n.tagName === 'IFRAME') {
123
+ const w = n.closest && n.closest(`.${WRAP_CLASS}`);
124
+ if (w) _fadeInIframe(n);
125
+ continue;
126
+ }
127
+ // If a subtree is added, look for iframes inside ad wrappers only.
128
+ const ifs = n.querySelectorAll ? n.querySelectorAll(`.${WRAP_CLASS} iframe`) : null;
129
+ if (ifs && ifs.length) ifs.forEach(_fadeInIframe);
130
+ }
131
+ }
132
+ }
133
+ } catch (e) {}
134
+ });
135
+
136
+ try {
137
+ _adFillObserver.observe(document.documentElement, { subtree: true, childList: true });
138
+ } catch (e) {}
139
+
140
+
99
141
 
100
142
  function markEmptyWrapper(id) {
101
143
  try {
@@ -205,6 +247,18 @@
205
247
  ['dns-prefetch', 'https://g.ezoic.net', false],
206
248
  ['preconnect', 'https://go.ezoic.net', true],
207
249
  ['dns-prefetch', 'https://go.ezoic.net', false],
250
+
251
+ // Google ad stack (helps Safeframe/GPT warm up)
252
+ ['preconnect', 'https://pagead2.googlesyndication.com', true],
253
+ ['dns-prefetch', 'https://pagead2.googlesyndication.com', false],
254
+ ['preconnect', 'https://securepubads.g.doubleclick.net', true],
255
+ ['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
256
+ ['preconnect', 'https://tpc.googlesyndication.com', true],
257
+ ['dns-prefetch', 'https://tpc.googlesyndication.com', false],
258
+ ['preconnect', 'https://googleads.g.doubleclick.net', true],
259
+ ['dns-prefetch', 'https://googleads.g.doubleclick.net', false],
260
+ ['preconnect', 'https://static.doubleclick.net', true],
261
+ ['dns-prefetch', 'https://static.doubleclick.net', false],
208
262
  ];
209
263
  for (const [rel, href, cors] of links) {
210
264
  const key = `${rel}|${href}`;
@@ -385,6 +439,7 @@ function buildWrap(id, kindClass, afterPos) {
385
439
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
386
440
  wrap.setAttribute('data-ezoic-after', String(afterPos));
387
441
  wrap.setAttribute('data-ezoic-wrapid', String(id));
442
+ wrap.setAttribute('data-ezoic-ts', String(Date.now()));
388
443
  wrap.style.width = '100%';
389
444
 
390
445
  const ph = document.createElement('div');
@@ -441,14 +496,23 @@ function buildWrap(id, kindClass, afterPos) {
441
496
  const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
442
497
  if (!wraps.length) return false;
443
498
 
444
- // Prefer a wrap far above the viewport
499
+ const now = Date.now();
500
+
501
+ // Only recycle wraps that are clearly out of view AND old enough.
502
+ // This avoids the "unstable" feeling where ads disappear when the user scrolls back a bit.
503
+ const MIN_AGE_MS = 20000; // 20s
504
+ const OFFSCREEN_PX = -5000; // far above viewport
505
+
445
506
  let victim = null;
446
507
  for (const w of wraps) {
447
508
  const r = w.getBoundingClientRect();
448
- if (r.bottom < -2000) { victim = w; break; }
509
+ const ts = parseInt(w.getAttribute('data-ezoic-ts') || '0', 10);
510
+ const ageOk = !ts || (now - ts) >= MIN_AGE_MS;
511
+ if (ageOk && r.bottom < OFFSCREEN_PX) { victim = w; break; }
449
512
  }
450
- // Otherwise remove the earliest one in the document
451
- if (!victim) victim = wraps[0];
513
+
514
+ // If nothing is eligible, do not recycle. We'll simply skip inserting new ads this run.
515
+ if (!victim) return false;
452
516
 
453
517
  // Unobserve placeholder if still observed
454
518
  try {
package/public/style.css CHANGED
@@ -38,3 +38,9 @@
38
38
  .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
39
39
  min-height: 0 !important;
40
40
  }
41
+
42
+ /* Avoid baseline gap under iframes (can look like extra space) */
43
+ .ezoic-ad iframe {
44
+ display: block !important;
45
+ vertical-align: top !important;
46
+ }