PageSourceSearch

https://ifmnoticias.com/js/promo.js?v=1790290084000

js ifmnoticias.com collected 2026-10-02 10:58:47 UTC 6,948 bytes, 169 lines download raw bytes

1// Client-side promo loader — fetches from /api/promo/serve/:zone and injects HTML
2// Empty containers are hidden via CSS (.sponsor-slot:empty { display:none })
3(function() {
4  'use strict';
5
6  // Sanitización básica de HTML para prevenir XSS (scripts, event handlers, javascript: hrefs)
7  function sanitizeHtml(html) {
8    var div = document.createElement('div');
9    div.innerHTML = html;
10    // Eliminar etiquetas script
11    var scripts = div.querySelectorAll('script');
12    for (var i = 0; i < scripts.length; i++) scripts[i].remove();
13    // Eliminar event handlers de todos los elementos
14    var all = div.querySelectorAll('*');
15    for (var i = 0; i < all.length; i++) {
16      var attrs = all[i].attributes;
17      for (var j = attrs.length - 1; j >= 0; j--) {
18        if (attrs[j].name.toLowerCase().startsWith('on')) {
19          all[i].removeAttribute(attrs[j].name);
20        }
21      }
22      // Eliminar hrefs con javascript:
23      if (all[i].hasAttribute('href') && all[i].getAttribute('href').trim().toLowerCase().startsWith('javascript:')) {
24        all[i].removeAttribute('href');
25      }
26    }
27    return div.innerHTML;
28  }
29
30  var containers = document.querySelectorAll('[data-promo]');
31  if (!containers.length) return;
32
33  var rotatingSlots = []; // Slots que rotan (no AdSense)
34  containers.forEach(function(el) { loadSlot(el); });
35
36  // En notas con video NO rotamos los anuncios: evita interrumpir la
37  // reproducción y los saltos de maquetación mientras el lector ve el video.
38  var hasVideo = !!document.querySelector('video, iframe[src*="youtube"], iframe[src*="vimeo"], iframe[src*="dailymotion"], iframe[src*="tiktok"]');
39
40  // Rotación automática cada 120 segundos para slots con campañas propias.
41  if (!hasVideo) {
42    setInterval(function() {
43      rotatingSlots.forEach(function(el) {
44        if (document.hidden) return; // No rotar si la pestaña no está visible
45        loadSlot(el, true);
46      });
47    }, 120000);
48  }
49
50  function loadSlot(el, isRotation) {
51    var zone = el.getAttribute('data-promo');
52    fetch('/api/promo/serve/' + zone)
53      .then(function(res) { return res.json(); })
54      .then(function(data) {
55        if (!data || data.type === 'empty') { return; }
56
57        var html = '';
58        // Limitar altura del creative para que no cubra más del 50% del viewport mobile (~340px en celulares estándar).
59        // Evita que un creative de 1200px de alto colapse la lectura.
60        var imgStyle = 'max-height:340px;width:auto;max-width:100%;margin:0 auto;display:block;object-fit:contain';
61        if (data.type === 'image' && data.imageUrl) {
62          var clickWrap = data.clickUrl
63            ? '<a href="/api/promo/click/' + data.id + '" target="_blank" rel="noopener sponsored">'
64            : '';
65          var clickEnd = data.clickUrl ? '</a>' : '';
66          html = clickWrap + '<img src="' + data.imageUrl + '" alt="' + (data.altText || '') + '" class="rounded-lg" style="' + imgStyle + '" loading="lazy">' + clickEnd;
67        } else if (data.type === 'video' && data.videoUrl) {
68          var vClickWrap = data.clickUrl
69            ? '<a href="/api/promo/click/' + data.id + '" target="_blank" rel="noopener sponsored">'
70            : '';
71          var vClickEnd = data.clickUrl ? '</a>' : '';
72          html = vClickWrap + '<video src="' + data.videoUrl + '" autoplay muted loop playsinline class="rounded-lg" style="' + imgStyle + '">' + (data.altText || '') + '</video>' + vClickEnd;
73        } else if (data.type === 'html' && data.htmlContent) {
74          html = sanitizeHtml(data.htmlContent);
75        } else if (data.type === 'adsense' && data.adsenseClient) {
76          if (!isRotation) renderAdsense(el, data); // No re-inyectar AdSense en rotación
77          return;
78        }
79
80        if (!html) { return; }
81        // Reset reserved CLS space once content loads
82        el.style.minHeight = '0';
83        // Transición suave en rotación
84        if (isRotation) {
85          el.style.opacity = '0';
86          setTimeout(function() {
87            el.innerHTML = html;
88            el.style.opacity = '1';
89          }, 200);
90        } else {
91          el.innerHTML = html;
92        }
93        // Registrar slot para rotación (solo campañas propias, no AdSense)
94        if (!isRotation && rotatingSlots.indexOf(el) === -1) {
95          rotatingSlots.push(el);
96        }
97        // Trackear impresión solo cuando el ad sea visible en el viewport
98        scheduleViewTrack(el, data.id);
99      })
100      .catch(function() {});
101  }
102
103  // Deduplicación: no recontar la misma campaña en la misma sesión de página
104  var trackedCampaigns = {};
105
106  // IntersectionObserver para contar impresiones solo cuando el ad es visible
107  var viewObserver = ('IntersectionObserver' in window)
108    ? new IntersectionObserver(function(entries) {
109        entries.forEach(function(entry) {
110          if (!entry.isIntersecting) return;
111          var el = entry.target;
112          var cid = el._pendingTrackId;
113          if (!cid) return;
114          // Solo contar 1 vez por campaña por pageview
115          if (trackedCampaigns[cid]) { viewObserver.unobserve(el); return; }
116          trackedCampaigns[cid] = true;
117          trackView(cid);
118          viewObserver.unobserve(el);
119        });
120      }, { threshold: 0.5 }) // Al menos 50% visible
121    : null;
122
123  function scheduleViewTrack(el, campaignId) {
124    if (!campaignId) return;
125    // Si ya se contó esta campaña en este pageview, no hacer nada
126    if (trackedCampaigns[campaignId]) return;
127    if (viewObserver) {
128      el._pendingTrackId = campaignId;
129      viewObserver.observe(el);
130    } else {
131      // Fallback sin IntersectionObserver: contar directo pero con dedup
132      if (!trackedCampaigns[campaignId]) {
133        trackedCampaigns[campaignId] = true;
134        trackView(campaignId);
135      }
136    }
137  }
138
139  function renderAdsense(el, data) {
140    el.style.minHeight = '0'; // Reset reserved CLS space
141    var ins = document.createElement('ins');
142    ins.className = 'adsbygoogle';
143    ins.style.display = 'block';
144    ins.setAttribute('data-ad-client', data.adsenseClient);
145    if (data.adsenseSlot) ins.setAttribute('data-ad-slot', data.adsenseSlot);
146    ins.setAttribute('data-ad-format', 'auto');
147    ins.setAttribute('data-full-width-responsive', 'true');
148    el.appendChild(ins);
149    try { (window.adsbygoogle = window.adsbygoogle || []).push({}); } catch(e) {}
150
151    // Si AdSense no llena el slot en 3s, colapsar el contenedor para evitar espacio blanco
152    setTimeout(function() {
153      var filled = ins.getAttribute('data-ad-status');
154      var h = ins.offsetHeight;
155      if (h < 10 || filled === 'unfilled') {
156        el.innerHTML = ''; // Vuelve a :empty → CSS lo oculta
157      }
158    }, 3000);
159  }
160
161  function trackView(id) {
162    if (!id) return;
163    try {
164      navigator.sendBeacon('/api/promo/view/' + id);
165    } catch (e) {
166      fetch('/api/promo/view/' + id, { method: 'POST' }).catch(function() {});
167    }
168  }
169})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.