nodebb-plugin-ezoic-infinite 1.6.55 → 1.6.57

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
@@ -4,127 +4,50 @@ const meta = require.main.require('./src/meta');
4
4
  const groups = require.main.require('./src/groups');
5
5
  const db = require.main.require('./src/database');
6
6
 
7
- const SETTINGS_KEY = 'ezoic-infinite';
8
7
  const plugin = {};
9
8
 
10
- function normalizeExcludedGroups(value) {
11
- if (!value) return [];
12
- if (Array.isArray(value)) return value;
13
- return String(value).split(',').map(s => s.trim()).filter(Boolean);
14
- }
15
-
16
- function parseBool(v, def = false) {
17
- if (v === undefined || v === null || v === '') return def;
18
- if (typeof v === 'boolean') return v;
19
- const s = String(v).toLowerCase();
20
- return s === '1' || s === 'true' || s === 'on' || s === 'yes';
21
- }
22
-
23
- async function getAllGroups() {
24
- let names = await db.getSortedSetRange('groups:createtime', 0, -1);
25
- if (!names || !names.length) {
26
- names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
27
- }
28
- const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
29
- const data = await groups.getGroupsData(filtered);
30
- // Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
31
- const valid = data.filter(g => g && g.name);
32
- valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
33
- return valid;
34
- }
35
- let _settingsCache = null;
36
- let _settingsCacheAt = 0;
37
- const SETTINGS_TTL = 30000; // 30s
38
-
9
+ // Helper pour garantir des valeurs par défaut
39
10
  async function getSettings() {
40
- const now = Date.now();
41
- if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
42
- const s = await meta.settings.get(SETTINGS_KEY);
43
- _settingsCacheAt = Date.now();
44
- _settingsCache = {
45
- // Between-post ads (simple blocks) in category topic list
46
- enableBetweenAds: parseBool(s.enableBetweenAds, true),
47
- showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
48
- placeholderIds: (s.placeholderIds || '').trim(),
49
- intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
50
-
51
- // Home/categories list ads (between categories on / or /categories)
52
- enableCategoryAds: parseBool(s.enableCategoryAds, false),
53
- showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
54
- categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
55
- intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
56
-
57
- // "Ad message" between replies (looks like a post)
58
- enableMessageAds: parseBool(s.enableMessageAds, false),
59
- showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
60
- messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
61
- messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
62
-
63
- excludedGroups: normalizeExcludedGroups(s.excludedGroups),
64
- };
65
- return _settingsCache;
66
- }
67
-
68
- async function isUserExcluded(uid, excludedGroups) {
69
- if (!uid || !excludedGroups.length) return false;
70
- const userGroups = await groups.getUserGroups([uid]);
71
- return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
11
+ const settings = await meta.settings.get('ezoic-infinite');
12
+ return {
13
+ enableBetweenAds: settings.enableBetweenAds === 'on',
14
+ showFirstTopicAd: settings.showFirstTopicAd === 'on',
15
+ placeholderIds: settings.placeholderIds || '',
16
+ intervalPosts: parseInt(settings.intervalPosts, 10) || 5,
17
+ enableMessageAds: settings.enableMessageAds === 'on',
18
+ showFirstMessageAd: settings.showFirstMessageAd === 'on',
19
+ messagePlaceholderIds: settings.messagePlaceholderIds || '',
20
+ messageIntervalPosts: parseInt(settings.messageIntervalPosts, 10) || 5,
21
+ excludedGroups: settings.excludedGroups || [],
22
+ };
72
23
  }
73
24
 
74
- plugin.onSettingsSet = function (data) {
75
- // Invalider le cache dès que les settings de ce plugin sont sauvegardés via l'ACP
76
- if (data && data.hash === SETTINGS_KEY) {
77
- _settingsCache = null;
78
- }
79
- };
80
-
81
- plugin.addAdminNavigation = async (header) => {
82
- header.plugins = header.plugins || [];
83
- header.plugins.push({
84
- route: '/plugins/ezoic-infinite',
85
- icon: 'fa-ad',
86
- name: 'Ezoic Infinite Ads'
87
- });
88
- return header;
89
- };
90
-
91
25
  plugin.init = async ({ router, middleware }) => {
92
- async function render(req, res) {
93
- const settings = await getSettings();
94
- const allGroups = await getAllGroups();
95
-
96
- res.render('admin/plugins/ezoic-infinite', {
97
- title: 'Ezoic Infinite Ads',
98
- ...settings,
99
- enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
100
- enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
101
- allGroups,
26
+ const render = async (req, res) => {
27
+ const settings = await getSettings();
28
+ const allGroups = await getAllGroups(); // Utilise votre fonction existante
29
+ res.render('admin/plugins/ezoic-infinite', { ...settings, allGroups });
30
+ };
31
+
32
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
33
+ router.get('/api/admin/plugins/ezoic-infinite', render);
34
+
35
+ // L'API que le client.js appelle
36
+ router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
37
+ const settings = await getSettings();
38
+ let excluded = false;
39
+
40
+ if (req.uid && settings.excludedGroups.length) {
41
+ excluded = await groups.isMemberOfAny(req.uid, settings.excludedGroups);
42
+ }
43
+
44
+ res.json({
45
+ ...settings,
46
+ excluded
47
+ });
102
48
  });
103
- }
104
-
105
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
106
- router.get('/api/admin/plugins/ezoic-infinite', render);
107
-
108
- router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
109
- const settings = await getSettings();
110
- const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
111
-
112
- res.json({
113
- excluded,
114
- enableBetweenAds: settings.enableBetweenAds,
115
- showFirstTopicAd: settings.showFirstTopicAd,
116
- placeholderIds: settings.placeholderIds,
117
- intervalPosts: settings.intervalPosts,
118
- enableCategoryAds: settings.enableCategoryAds,
119
- showFirstCategoryAd: settings.showFirstCategoryAd,
120
- categoryPlaceholderIds: settings.categoryPlaceholderIds,
121
- intervalCategories: settings.intervalCategories,
122
- enableMessageAds: settings.enableMessageAds,
123
- showFirstMessageAd: settings.showFirstMessageAd,
124
- messagePlaceholderIds: settings.messagePlaceholderIds,
125
- messageIntervalPosts: settings.messageIntervalPosts,
126
- });
127
- });
128
49
  };
129
50
 
130
- module.exports = plugin;
51
+ // ... gardez vos fonctions getAllGroups et addAdminNavigation ...
52
+
53
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.55",
3
+ "version": "1.6.57",
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,25 +1,50 @@
1
1
  (function () {
2
2
  'use strict';
3
3
 
4
- // --- 1. VARIABLES ET CONFIG ---
5
4
  const WRAP_CLASS = 'nodebb-ezoic-wrap';
6
5
  const PINNED_ATTR = 'data-ezoic-pinned';
7
6
  const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
8
7
 
9
8
  let config = null;
10
9
  let usedIds = new Set();
10
+ let isEzoicEnabled = false; // Flag crucial pour corriger vos logs d'erreur
11
11
  let scheduleTimer = null;
12
12
 
13
- // --- 2. SÉLECTEURS DE THÈMES ---
13
+ // --- SÉLECTEURS ---
14
14
  function getTopicList() {
15
- return document.querySelector('.category [component="category"], .categories [component="categories"], [component="topic/list"], .topic-list');
15
+ return document.querySelector('[component="topic/list"], [component="category"], [component="categories"]');
16
16
  }
17
17
 
18
18
  function getPostList() {
19
19
  return document.querySelector('[component="topic"], [component="post/list"]');
20
20
  }
21
21
 
22
- // --- 3. LOGIQUE DES POOLS D'ID ---
22
+ // --- LOGIQUE EZOIC ---
23
+ function callEzoic(idsToDefine) {
24
+ if (!window.ezstandalone || !idsToDefine.length) return;
25
+
26
+ try {
27
+ // 1. Définir les placeholders
28
+ window.ezstandalone.define(idsToDefine);
29
+
30
+ // 2. Logique Enable vs Refresh (Correction de vos erreurs log)
31
+ if (!isEzoicEnabled && !window.ezstandalone.enabled) {
32
+ window.ezstandalone.enable();
33
+ window.ezstandalone.display();
34
+ isEzoicEnabled = true;
35
+ console.log('[Ezoic] First Enable & Display');
36
+ } else {
37
+ // On attend un cycle pour s'assurer qu'Ezoic est prêt pour le refresh
38
+ setTimeout(() => {
39
+ window.ezstandalone.refresh();
40
+ console.log('[Ezoic] Refreshing existing instance');
41
+ }, 100);
42
+ }
43
+ } catch (e) {
44
+ console.warn('[Ezoic] SDK Error:', e);
45
+ }
46
+ }
47
+
23
48
  function getNextId(poolStr) {
24
49
  if (!poolStr) return null;
25
50
  const ids = poolStr.split(/[\s,]+/).filter(Boolean);
@@ -33,131 +58,93 @@
33
58
  return null;
34
59
  }
35
60
 
36
- // --- 4. APPEL EZOIC (REFRESH & SHOW) ---
37
- function callEzoic(id) {
38
- if (window.ezstandalone) {
39
- try {
40
- const nid = parseInt(id, 10);
41
- window.ezstandalone.define(nid);
42
- if (!window.ezstandalone.enabled) {
43
- window.ezstandalone.enable();
44
- window.ezstandalone.display();
45
- } else {
46
- window.ezstandalone.refresh();
47
- }
48
- } catch (e) { console.warn('[ezoic] refresh error', id, e); }
49
- }
50
- }
51
-
52
- // --- 5. INJECTION SÉCURISÉE (ANTI-PILEUP) ---
61
+ // --- INJECTION ---
53
62
  function inject() {
54
63
  if (!config || config.excluded) return;
64
+ const newIds = [];
55
65
 
56
- // A. LISTE DES SUJETS (Topics)
66
+ // A. Topics / Categories
57
67
  const ul = getTopicList();
58
68
  if (ul && config.enableBetweenAds) {
59
- // Nettoyage des pubs orphelines (celles qui n'ont plus de LI au dessus à cause du scroll up)
60
- const wraps = ul.querySelectorAll('.' + WRAP_CLASS);
61
- wraps.forEach(w => {
69
+ // Nettoyage anti-pileup (remontée)
70
+ ul.querySelectorAll('.' + WRAP_CLASS).forEach(w => {
62
71
  if (!w.previousElementSibling || w.previousElementSibling.tagName !== 'LI') w.remove();
63
72
  });
64
73
 
65
74
  const items = Array.from(ul.children).filter(c => c.tagName === 'LI' && !c.classList.contains(WRAP_CLASS));
66
- if (items.length > 0) {
67
- // Pub n°1 (Pinned)
68
- if (config.showFirstTopicAd && !ul.querySelector(`[${PINNED_ATTR}="true"]`)) {
69
- insertAd(items[0], getNextId(config.placeholderIds), true);
75
+
76
+ // First Ad
77
+ if (config.showFirstTopicAd && !ul.querySelector(`[${PINNED_ATTR}="true"]`)) {
78
+ const id = getNextId(config.placeholderIds);
79
+ if (id) {
80
+ insertAd(items[0], id, true);
81
+ newIds.push(parseInt(id, 10));
70
82
  }
83
+ }
71
84
 
72
- // Pubs d'intervalles
73
- const interval = parseInt(config.intervalPosts, 10) || 5;
74
- items.forEach((li, idx) => {
75
- const pos = idx + 1;
76
- if (pos > 1 && pos % interval === 0) {
77
- // Vérifie si ce LI a déjà une pub juste après lui pour éviter les doublons
78
- const next = li.nextElementSibling;
79
- if (!next || !next.classList.contains(WRAP_CLASS)) {
80
- insertAd(li, getNextId(config.placeholderIds), false);
85
+ // Intervals
86
+ const interval = parseInt(config.intervalPosts, 10) || 5;
87
+ items.forEach((li, idx) => {
88
+ if ((idx + 1) % interval === 0 && idx > 0) {
89
+ if (!li.nextElementSibling || !li.nextElementSibling.classList.contains(WRAP_CLASS)) {
90
+ const id = getNextId(config.placeholderIds);
91
+ if (id) {
92
+ insertAd(li, id, false);
93
+ newIds.push(parseInt(id, 10));
81
94
  }
82
95
  }
83
- });
84
- }
96
+ }
97
+ });
85
98
  }
86
99
 
87
- // B. LISTE DES MESSAGES (Posts)
100
+ // B. Messages (Posts)
88
101
  const postList = getPostList();
89
102
  if (postList && config.enableMessageAds) {
90
103
  const posts = Array.from(postList.querySelectorAll('[component="post"]'));
91
104
  if (config.showFirstMessageAd && posts.length > 0 && !postList.querySelector('.ezoic-msg-first')) {
92
- insertAd(posts[0], getNextId(config.messagePlaceholderIds), false, 'ezoic-msg-first');
93
- }
94
-
95
- const msgInterval = parseInt(config.messageIntervalPosts, 10) || 5;
96
- if (msgInterval > 0) {
97
- posts.forEach((post, idx) => {
98
- const pPos = idx + 1;
99
- if (pPos > 1 && pPos % msgInterval === 0) {
100
- if (!post.nextElementSibling || !post.nextElementSibling.classList.contains(WRAP_CLASS)) {
101
- insertAd(post, getNextId(config.messagePlaceholderIds), false);
102
- }
103
- }
104
- });
105
+ const id = getNextId(config.messagePlaceholderIds);
106
+ if (id) {
107
+ insertAd(posts[0], id, false, 'ezoic-msg-first');
108
+ newIds.push(parseInt(id, 10));
109
+ }
105
110
  }
106
111
  }
112
+
113
+ if (newIds.length > 0) {
114
+ callEzoic(newIds);
115
+ }
107
116
  }
108
117
 
109
118
  function insertAd(target, id, isPinned, extraClass = '') {
110
- if (!target || !id) return;
111
119
  const wrap = document.createElement('div');
112
120
  wrap.className = `${WRAP_CLASS} ezoic-ad-between ${extraClass}`;
113
121
  if (isPinned) wrap.setAttribute(PINNED_ATTR, 'true');
114
-
115
- const placeholder = document.createElement('div');
116
- placeholder.id = PLACEHOLDER_PREFIX + id;
117
- wrap.appendChild(placeholder);
118
-
122
+ wrap.innerHTML = `<div id="${PLACEHOLDER_PREFIX}${id}"></div>`;
119
123
  target.after(wrap);
120
-
121
- // On laisse le DOM respirer 50ms avant d'appeler Ezoic
122
- setTimeout(() => callEzoic(id), 50);
123
124
  }
124
125
 
125
- // --- 6. INITIALISATION ET HOOKS ---
126
- async function fetchConfig() {
126
+ // --- INIT ---
127
+ async function init() {
127
128
  try {
128
129
  const res = await fetch('/api/plugins/ezoic-infinite/config');
129
130
  config = await res.json();
130
- } catch (e) { console.error('[ezoic] config fetch failed'); }
131
- }
132
-
133
- function schedule() {
134
- if (scheduleTimer) clearTimeout(scheduleTimer);
135
- scheduleTimer = setTimeout(inject, 250);
136
- }
131
+
132
+ inject();
137
133
 
138
- function init() {
139
- fetchConfig().then(() => {
140
- schedule();
141
-
142
- // MutationObserver pour le scroll infini
143
- const body = document.body;
144
- const mo = new MutationObserver((mutations) => {
145
- for (let m of mutations) {
146
- if (m.addedNodes.length > 0) {
147
- schedule();
148
- break;
149
- }
134
+ const observer = new MutationObserver((mutations) => {
135
+ if (mutations.some(m => m.addedNodes.length > 0)) {
136
+ if (scheduleTimer) clearTimeout(scheduleTimer);
137
+ scheduleTimer = setTimeout(inject, 300);
150
138
  }
151
139
  });
152
- mo.observe(body, { childList: true, subtree: true });
153
- });
140
+ observer.observe(document.body, { childList: true, subtree: true });
154
141
 
155
- // Compatibilité jQuery/NodeBB Events
156
- if (window.jQuery) {
157
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', () => {
158
- setTimeout(schedule, 500);
159
- });
160
- }
142
+ if (window.jQuery) {
143
+ window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', () => {
144
+ setTimeout(inject, 500);
145
+ });
146
+ }
147
+ } catch (e) {}
161
148
  }
162
149
 
163
150
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
package/public/style.css CHANGED
@@ -1,100 +1,80 @@
1
- /*
2
- Keep our NodeBB-inserted wrappers CLS-safe.
3
- NOTE: must not rely on `.ezoic-ad` because Ezoic uses that class internally.
4
- */
1
+ /* --- Conteneur principal inséré entre les sujets/messages --- */
5
2
  .nodebb-ezoic-wrap {
6
- display: block;
7
- width: 100%;
8
- margin: 0 !important;
9
- padding: 0 !important;
10
- overflow: hidden;
3
+ display: block !important;
4
+ width: 100% !important;
5
+ clear: both !important;
6
+ margin: 20px 0 !important;
7
+ padding: 0 !important;
8
+ /* Important : Empêche les éléments de déborder lors du recyclage du DOM */
9
+ overflow: hidden;
10
+ /* Améliore les performances de rendu sur NodeBB 4.x */
11
+ contain: layout style;
12
+ /* Assure une visibilité minimale pour que le SDK Ezoic puisse mesurer l'emplacement */
13
+ min-height: 50px;
11
14
  }
12
15
 
13
- .nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
14
- margin: 0 !important;
15
- padding: 0 !important;
16
- /* Keep the placeholder measurable (IO) but visually negligible */
17
- min-height: 1px;
16
+ /* --- Protection spécifique pour la 1ère publicité (Pinned) --- */
17
+ .nodebb-ezoic-wrap[data-ezoic-pinned="true"] {
18
+ /* Réserve une hauteur pour éviter le "Layout Shift" (saut d'écran) au scroll up */
19
+ min-height: 150px;
20
+ border-bottom: 1px solid rgba(0,0,0,0.05);
21
+ margin-top: 5px !important;
18
22
  }
19
23
 
20
- /* If Ezoic wraps inside our wrapper, keep it tight */
21
- .nodebb-ezoic-wrap span.ezoic-ad,
22
- .nodebb-ezoic-wrap .ezoic-ad {
23
- margin: 0 !important;
24
- padding: 0 !important;
24
+ /* --- Le placeholder Ezoic interne --- */
25
+ [id^="ezoic-pub-ad-placeholder-"] {
26
+ margin: 0 auto !important;
27
+ padding: 0 !important;
28
+ text-align: center;
29
+ min-height: 1px;
30
+ line-height: 0;
25
31
  }
26
32
 
27
- /* Remove the classic "gap under iframe" (baseline/inline-block) */
28
- .nodebb-ezoic-wrap,
29
- .nodebb-ezoic-wrap * {
30
- line-height: 0 !important;
31
- font-size: 0 !important;
33
+ /* --- Forcer le centrage et la réactivité des pubs Ezoic --- */
34
+ .nodebb-ezoic-wrap span.ezoic-ad,
35
+ .nodebb-ezoic-wrap .ezoic-ad,
36
+ .nodebb-ezoic-wrap iframe {
37
+ display: block !important;
38
+ margin: 0 auto !important;
39
+ max-width: 100% !important;
32
40
  }
33
41
 
34
- .nodebb-ezoic-wrap iframe,
35
- .nodebb-ezoic-wrap div[id$="__container__"] iframe {
36
- display: block !important;
37
- vertical-align: top !important;
42
+ /* --- Gestion des blocs vides (Anti-Trous blancs) --- */
43
+ /* Si Ezoic ne remplit pas l'emplacement ou si le script le vide,
44
+ on réduit l'espace pour ne pas casser le design du forum */
45
+ .nodebb-ezoic-wrap:empty {
46
+ height: 0 !important;
47
+ min-height: 0 !important;
48
+ margin: 0 !important;
49
+ border: none !important;
38
50
  }
39
51
 
40
- .nodebb-ezoic-wrap div[id$="__container__"] {
41
- display: block !important;
42
- line-height: 0 !important;
52
+ /* --- Adaptation pour les messages (Topic View) --- */
53
+ .ezoic-msg-first {
54
+ margin-bottom: 35px !important;
55
+ padding-bottom: 15px !important;
56
+ border-bottom: 1px solid var(--border-color, #eee);
43
57
  }
44
58
 
45
-
46
- /* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
47
- .nodebb-ezoic-wrap.is-empty {
48
- display: block !important;
49
- margin: 0 !important;
50
- padding: 0 !important;
51
- /* Don't fully collapse (can prevent fill / triggers "unused"), keep it at 1px */
52
- height: 1px !important;
53
- min-height: 1px !important;
54
- overflow: hidden !important;
59
+ /* --- Correction pour le mode sombre (NodeBB 4 Harmony) --- */
60
+ [data-theme="dark"] .nodebb-ezoic-wrap[data-ezoic-pinned="true"] {
61
+ border-bottom-color: rgba(255,255,255,0.1);
55
62
  }
56
63
 
57
- /*
58
- Optional: also neutralize spacing on native Ezoic `.ezoic-ad` blocks.
59
- (Keeps your previous "CSS very good" behavior.)
60
- */
61
- .ezoic-ad {
62
- margin: 0 !important;
63
- padding: 0 !important;
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;
64
+ /* --- Neutralisation des marges Ezoic sur mobile --- */
65
+ @media (max-width: 767px) {
66
+ .nodebb-ezoic-wrap {
67
+ margin: 10px 0 !important;
68
+ min-height: 30px;
69
+ }
70
+ .nodebb-ezoic-wrap[data-ezoic-pinned="true"] {
71
+ min-height: 100px;
72
+ }
70
73
  }
71
74
 
72
- /* Ensure Ezoic reportline doesn't affect layout */
73
- .nodebb-ezoic-wrap .reportline{position:absolute!important;}
74
-
75
- /* Ezoic sometimes injects `position: sticky` inside placements. In long NodeBB topics,
76
- this can create "gliding" and sudden disappear/reappear effects while scrolling.
77
- We neutralize sticky positioning *inside our injected wrappers* only. */
75
+ /* --- Sécurité Anti-Sticky ---
76
+ Ezoic injecte parfois du 'position: sticky' qui fait "flotter" les pubs
77
+ de manière erratique lors du scroll infini. On le neutralise ici. */
78
78
  .nodebb-ezoic-wrap .ezads-sticky-intradiv {
79
- position: static !important;
80
- top: auto !important;
81
- }
82
-
83
-
84
- /* ===== V17 host styling ===== */
85
- li.nodebb-ezoic-host { list-style: none; width: 100%; display: block; }
86
- li.nodebb-ezoic-host > .nodebb-ezoic-wrap.ezoic-ad-between { width: 100%; display: block; }
87
- /* ===== /V17 ===== */
88
-
89
- .nodebb-ezoic-wrap {
90
- display: block;
91
- width: 100%;
92
- clear: both;
93
- margin: 20px 0 !important; /* Ajoute un peu d'espace pour le visuel */
94
- min-height: 50px; /* Évite que le bloc ne soit écrasé à 0px */
95
- }
96
-
97
- /* Si la pub est vide, on garde un petit espace pour éviter le saut au scroll up */
98
- .nodebb-ezoic-wrap:empty {
99
- min-height: 1px;
79
+ position: static !important;
100
80
  }