1(function () { 2 function enhanceAdAccessibility(container) { 3 var links = container.querySelectorAll("a"); 4 links.forEach(function (link) { 5 if (!link.getAttribute("target")) { 6 link.setAttribute("target", "_blank"); 7 link.setAttribute("rel", "noopener noreferrer"); 8 } 9 10 if (link.getAttribute("aria-label") || link.textContent.trim()) return; 11 12 var img = link.querySelector("img"); 13 if (img) { 14 var alt = (img.getAttribute("alt") || "").trim(); 15 if (alt) { 16 link.setAttribute("aria-label", alt); 17 } else { 18 link.setAttribute("aria-label", "Advertisement"); 19 } 20 img.setAttribute("alt", ""); 21 } else { 22 link.setAttribute("aria-label", "Advertisement"); 23 } 24 }); 25 } 26 27 function waitForImages(container, callback) { 28 var imgs = container.querySelectorAll("img"); 29 if (!imgs.length) { 30 callback(); 31 return; 32 } 33 34 var remaining = imgs.length; 35 function done() { 36 remaining--; 37 if (remaining <= 0) callback(); 38 } 39 40 for (var i = 0; i < imgs.length; i++) { 41 var img = imgs[i]; 42 if (img.complete) done(); 43 else { 44 img.addEventListener("load", done); 45 img.addEventListener("error", done); 46 } 47 } 48 } 49 50 function stabilizeSlideHeight(container, slides) { 51 var maxHeight = 0; 52 for (var i = 0; i < slides.length; i++) { 53 slides[i].classList.add("is-measuring"); 54 maxHeight = Math.max(maxHeight, slides[i].offsetHeight); 55 slides[i].classList.remove("is-measuring"); 56 } 57 if (maxHeight > 0) { 58 container.style.minHeight = maxHeight + "px"; 59 } 60 container.classList.add("is-ready"); 61 } 62 63 function clearAdInterval(el) { 64 var id = el.dataset.adIntervalId; 65 if (id) { 66 clearInterval(parseInt(id, 10)); 67 delete el.dataset.adIntervalId; 68 } 69 } 70 71 function recordImpression(url, adId) { 72 if (!adId) return; 73 var baseUrl = url.split("/adrotate/serve")[0]; 74 fetch(baseUrl + "/adrotate/serve", { 75 method: "POST", 76 headers: { "Content-Type": "application/json" }, 77 body: JSON.stringify({ 78 action: "impression", 79 adId: parseInt(adId, 10), 80 }), 81 }).catch(function () {}); 82 } 83 84 function showSlide(slides, index) { 85 for (var i = 0; i < slides.length; i++) { 86 slides[i].classList.toggle("is-active", i === index); 87 } 88 } 89 90 function preloadSlideImage(slide, callback) { 91 var img = slide.querySelector("img"); 92 if (!img) { 93 callback(); 94 return; 95 } 96 97 img.setAttribute("loading", "eager"); 98 99 if (img.complete && img.naturalWidth > 0) { 100 callback(); 101 return; 102 } 103 104 function onDone() { 105 img.removeEventListener("load", onDone); 106 img.removeEventListener("error", onDone); 107 callback(); 108 } 109 110 img.addEventListener("load", onDone); 111 img.addEventListener("error", onDone); 112 } 113 114 function startRotation(el, slides, url, speed) { 115 clearAdInterval(el); 116 117 var currentIndex = 0; 118 var isTransitioning = false; 119 120 var intervalId = setInterval(function () { 121 if (!document.body.contains(el)) { 122 clearInterval(intervalId); 123 return; 124 } 125 if (isTransitioning) return; 126 127 isTransitioning = true; 128 var nextIndex = (currentIndex + 1) % slides.length; 129 var nextSlide = slides[nextIndex]; 130 131 preloadSlideImage(nextSlide, function () { 132 showSlide(slides, nextIndex); 133 recordImpression(url, nextSlide.getAttribute("data-ad-id")); 134 currentIndex = nextIndex; 135 136 window.setTimeout(function () { 137 isTransitioning = false; 138 }, 500); 139 }); 140 }, speed); 141 142 el.dataset.adIntervalId = String(intervalId); 143 } 144 145 function parseRatio(ratio) { 146 if (!ratio) return null; 147 var parts = ratio.split("/"); 148 if (parts.length === 2) { 149 var w = parseFloat(parts[0]); 150 var h = parseFloat(parts[1]); 151 if (w && h) { 152 return { 153 width: Math.round(w * 100), 154 height: Math.round(h * 100), 155 }; 156 } 157 } 158 return null; 159 } 160 161 function getDefaultImageDimensions(el) { 162 var ratioAttr = el.getAttribute("data-ad-ratio"); 163 var parsed = parseRatio(ratioAttr); 164 if (parsed) return parsed; 165 166 var minHeight = el.getAttribute("data-ad-min-height") || "250px"; 167 var heightNum = parseInt(minHeight, 10); 168 if (!heightNum) heightNum = 250; 169 return { width: Math.round(heightNum * 1.2), height: heightNum }; 170 } 171 172 function setImageDimensions(el) { 173 var dims = getDefaultImageDimensions(el); 174 var imgs = el.querySelectorAll(".ad-slide img, .ad-block-item img");
175 imgs.forEach(function (img) { 176 if (!img.hasAttribute("width")) { 177 img.setAttribute("width", String(dims.width)); 178 } 179 if (!img.hasAttribute("height")) { 180 img.setAttribute("height", String(dims.height)); 181 } 182 }); 183 } 184 185 /** 186 * Avoid Mixed Content: HTTPS pages must not fetch http://⦠ad endpoints. 187 * Prefer same-origin relative /adrotate/serve when an absolute http URL was baked in. 188 */ 189 function normalizeAdApiUrl(url) { 190 if (!url) return url; 191 try { 192 if (url.charAt(0) === "/") return url; 193 if (typeof location !== "undefined" && location.protocol === "https:") { 194 var parsed = new URL(url, location.href); 195 if (parsed.protocol === "http:" && parsed.pathname.indexOf("/adrotate/") === 0) { 196 return parsed.pathname + parsed.search; 197 } 198 } 199 } catch (e) {} 200 return url; 201 } 202 203 function initEl(el) { 204 if (el.dataset.adInitialized === "true") return; 205 el.dataset.adInitialized = "true"; 206 207 var url = normalizeAdApiUrl(el.getAttribute("data-ad-api")); 208 if (!url) return; 209 210 fetch(url, { cache: "no-store" }) 211 .then(function (r) { 212 if (!r.ok) return null; 213 return r.json(); 214 }) 215 .then(function (data) { 216 if (!data || !data.ads || !data.ads.length) { 217 el.innerHTML = ""; 218 return; 219 } 220 221 var ads = data.ads; 222 var modus = data.groupModus || "dynamic"; 223 var speed = 224 data.adSpeed || 225 parseInt(el.getAttribute("data-ad-rotate") || "7000", 10); 226 227 // Optional: pin this banner to a single ad by 0-based index 228 // (e.g. first/second/third in-article slot â ad 0/1/2). 229 var adIndexAttr = el.getAttribute("data-ad-index"); 230 var pinnedIndex = 231 adIndexAttr !== null && adIndexAttr !== "" 232 ? parseInt(adIndexAttr, 10) 233 : NaN; 234 if (!isNaN(pinnedIndex) && pinnedIndex >= 0) { 235 if (pinnedIndex >= ads.length) { 236 el.innerHTML = ""; 237 return; 238 } 239 ads = [ads[pinnedIndex]]; 240 // Single ad: never rotate or render as multi-ad block 241 modus = "dynamic"; 242 } 243 244 if (modus === "block") { 245 var html = 246 '<div class="ad-block-grid flex flex-wrap gap-4 justify-center w-full">'; 247 for (var i = 0; i < ads.length; i++) { 248 html += '<div class="ad-block-item">' + ads[i].html + "</div>"; 249 } 250 html += "</div>"; 251 el.innerHTML = html; 252 enhanceAdAccessibility(el); 253 setImageDimensions(el); 254 } else { 255 var html = '<div class="ad-slides-container">'; 256 for (var j = 0; j < ads.length; j++) { 257 var activeClass = j === 0 ? " is-active" : ""; 258 html += 259 '<div class="ad-slide' + 260 activeClass + 261 '" data-ad-id="' + 262 ads[j].id + 263 '">' + 264 ads[j].html + 265 "</div>"; 266 } 267 html += "</div>"; 268 el.innerHTML = html; 269 enhanceAdAccessibility(el); 270 setImageDimensions(el); 271 272 var adImages = el.querySelectorAll(".ad-slide img"); 273 adImages.forEach(function (img, index) { 274 if (!img.hasAttribute("decoding")) { 275 img.setAttribute("decoding", "async"); 276 } 277 // Only the first/active slide needs to load eagerly. 278 // Hidden slides can lazy-load to reduce initial payload. 279 if (index === 0) { 280 img.setAttribute("loading", "eager"); 281 img.setAttribute("fetchpriority", "low"); 282 } else { 283 img.setAttribute("loading", "lazy"); 284 } 285 }); 286 287 var container = el.querySelector(".ad-slides-container"); 288 var slides = el.querySelectorAll(".ad-slide"); 289 290 waitForImages(el, function () { 291 stabilizeSlideHeight(container, slides); 292 293 if (ads.length > 1) { 294 startRotation(el, slides, url, speed); 295 } 296 }); 297 } 298 }) 299 .catch(function () { 300 el.innerHTML = ""; 301 }); 302 } 303 304 function observeEl(el) { 305 if (!("IntersectionObserver" in window)) { 306 initEl(el); 307 return; 308 } 309 310 var observer = new IntersectionObserver( 311 function (entries) { 312 for (var i = 0; i < entries.length; i++) { 313 if (entries[i].isIntersecting) { 314 initEl(el); 315 observer.disconnect(); 316 break; 317 } 318 } 319 }, 320 // Keep ads out of the LCP critical path (was 300px â fetched too early on mobile) 321 { rootMargin: "80px 0px", threshold: 0.01 } 322 ); 323 324 observer.observe(el); 325 } 326
327 function init() { 328 var els = document.querySelectorAll(".ad-banner:not([data-ad-initialized])"); 329 els.forEach(observeEl); 330 } 331 332 function startWhenIdle() { 333 var run = function () { 334 init(); 335 }; 336 if ("requestIdleCallback" in window) { 337 requestIdleCallback(run, { timeout: 2500 }); 338 } else { 339 setTimeout(run, 200); 340 } 341 } 342 343 // After full load so LCP image wins bandwidth on slow mobile 344 if (document.readyState === "complete") { 345 startWhenIdle(); 346 } else { 347 window.addEventListener("load", startWhenIdle); 348 } 349 350 document.addEventListener("astro:page-load", startWhenIdle); 351})();
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.