1/** 2 * Seznam Ads Client-Side Manager 3 * 4 * Handles ALL Seznam ad functionality client-side: 5 * 1. Detecting Seznam visitors via UTM params, referrer, or cookie 6 * 2. Managing the isFromSeznam cookie (set, prolong, clear) 7 * 3. Dynamically loading the Seznam SSP script 8 * 4. Creating and injecting ad containers based on page type and device 9 * 5. Requesting ads from Seznam SSP 10 * 6. Handling the mobile pop-up ad (zone 397782) on single posts 11 * 7. MutationObserver for dynamically loaded posts (infinite scroll) 12 */ 13(function () { 14 'use strict'; 15 16 // ========== Configuration ========== 17 18 const CONFIG = { 19 SITE_DOMAIN: 'denikalarm.cz', 20 SEZNAM_DOMAIN: 'www.seznam.cz', 21 // Seznam now identifies all distribution channels with a single medium 22 // (seznam_distribuce); individual channels are distinguished by campaign. 23 VALID_MEDIUMS: ['seznam_distribuce'], 24 VALID_CAMPAIGNS: [ 25 'hp_feed', // Newsfeed 26 'hp_box', // HP Boxy 27 'novinky', // Novinky / Dennà tisk 28 'sbrowser', // Seznam ProhlÞeÄ 29 'trending_topics', // Trending Topics 30 'asistent', // Asistent 31 'aifunkce', // AI funkce 32 'lide' // Lidé 33 ], 34 COOKIE_DURATION_MS: 30 * 60 * 1000, // 30 minutes 35 SCRIPTS: { 36 ssp: 'https://ssp.seznam.cz/static/js/ssp.js' 37 }, 38 TEST_MODE: false, // Enable for fill rate validation 39 DEBUG: false // Enable console logging 40 }; 41 42 // Ad zone definitions 43 const AD_ZONES = { 44 // Desktop zones 45 desktopLeaderboard: { zoneId: 150335, width: 970, height: 210 }, 46 desktopInContent: { zoneId: 215091, width: 480, height: 300 }, 47 desktopAfterContent: { zoneId: 150350, width: 111, height: 111 }, 48 desktopFooterRectangle: { zoneId: 150355, width: 970, height: 310 }, 49 50 // Mobile zones 51 mobileFirstPara: { zoneId: 150360, width: 480, height: 480 }, 52 mobileRepeating: { zoneId: 242045, width: 480, height: 480 }, 53 mobileAfterContent: { zoneId: 150365, width: 111, height: 111 }, 54 mobileFooter: { zoneId: 150370, width: 480, height: 480 }, 55 56 // Mobile pop-up reload (replaces former "Pop up mobil" zone 241544) 57 mobilePopupReload: { zoneId: 397782, width: 500, height: 200 } 58 }; 59 60 // State 61 const state = { 62 isSeznamVisitor: false, 63 scriptsLoaded: false, 64 pendingAds: [], 65 initialized: false, 66 pageType: null, // 'single', 'archive', 'home', 'search', 'category' 67 screenType: null // 'mobile', 'desktop' 68 }; 69 70 // ========== Logging ========== 71 72 function log(msg, data = null) { 73 if (!CONFIG.DEBUG) return; 74 const prefix = '[SeznamAds]'; 75 if (data !== null) { 76 console.log(`${prefix} ${msg}`, data); 77 } else { 78 console.log(`${prefix} ${msg}`); 79 } 80 } 81 82 // ========== Cookie Utilities ========== 83 84 function getCookie(name) { 85 const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); 86 return match ? match[2] : null; 87 } 88 89 function setCookie(name, value, durationMs) { 90 const expires = new Date(Date.now() + durationMs).toUTCString(); 91 document.cookie = `${name}=${value}; path=/; expires=${expires}; SameSite=Lax`; 92 log(`Cookie set: ${name}=${value}`); 93 } 94 95 function clearCookie(name) { 96 document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`; 97 log(`Cookie cleared: ${name}`); 98 } 99 100 // ========== Detection Logic ========== 101 102 function detectSeznamVisitor() { 103 const params = new URLSearchParams(window.location.search); 104 const referer = document.referrer; 105 const hasSeznamCookie = getCookie('isFromSeznam') === 'true'; 106 107 // Check if coming from Seznam via UTM params 108 const utmSource = params.get('utm_source'); 109 const utmMedium = params.get('utm_medium'); 110 const utmCampaign = params.get('utm_campaign'); 111 const isFromSeznamUtm = utmSource === CONFIG.SEZNAM_DOMAIN && 112 CONFIG.VALID_MEDIUMS.includes(utmMedium) && 113 CONFIG.VALID_CAMPAIGNS.includes(utmCampaign); 114 115 // Check if coming from Seznam via referrer 116 const isFromSeznamReferer = referer.includes(CONFIG.SEZNAM_DOMAIN); 117 118 // Check if internal navigation 119 const isInternalNav = referer.includes(CONFIG.SITE_DOMAIN); 120 121 log('Detection:', { utmSource, utmMedium, utmCampaign, isFromSeznamUtm, isFromSeznamReferer, isInternalNav, hasSez
121namCookie }); 122 123 let isSeznamVisitor = false; 124 125 if (isFromSeznamUtm || isFromSeznamReferer) { 126 // Direct arrival from Seznam 127 isSeznamVisitor = true; 128 setCookie('isFromSeznam', 'true', CONFIG.COOKIE_DURATION_MS); 129 } else if (hasSeznamCookie) { 130 if (isInternalNav) { 131 // Internal navigation - prolong cookie 132 isSeznamVisitor = true; 133 setCookie('isFromSeznam', 'true', CONFIG.COOKIE_DURATION_MS); 134 } else { 135 // External referrer or direct navigation - clear cookie 136 isSeznamVisitor = false; 137 clearCookie('isFromSeznam'); 138 } 139 } 140 141 log(`Visitor status: ${isSeznamVisitor ? 'Seznam' : 'Non-Seznam'}`); 142 return isSeznamVisitor; 143 } 144 145 // ========== Page Type Detection ========== 146 147 function detectPageType() { 148 const body = document.body; 149 150 if (body.classList.contains('single') || body.classList.contains('single-post')) { 151 return 'single'; 152 } 153 if (body.classList.contains('home')) { 154 return 'home'; 155 } 156 if (body.classList.contains('search')) { 157 return 'search'; 158 } 159 if (body.classList.contains('category')) { 160 return 'category'; 161 } 162 if (body.classList.contains('archive') || body.classList.contains('tag')) { 163 return 'archive'; 164 } 165 166 return 'other'; 167 } 168 169 function detectScreenType() { 170 return window.innerWidth <= 768 ? 'mobile' : 'desktop'; 171 } 172 173 // ========== Script Loading ========== 174 175 function loadScript(src) { 176 return new Promise((resolve, reject) => { 177 log(`Loading script: ${src}`); 178 const script = document.createElement('script'); 179 script.src = src; 180 script.async = true; 181 182 script.onload = () => { 183 log(`Script loaded: ${src}`); 184 resolve(); 185 }; 186 187 script.onerror = (err) => { 188 log(`Script failed: ${src}`, err); 189 reject(err); 190 }; 191 192 document.head.appendChild(script); 193 }); 194 } 195 196 function waitForSSSP(timeout = 5000) { 197 return new Promise((resolve, reject) => { 198 const start = Date.now(); 199 200 function check() { 201 if (window.sssp && typeof window.sssp.getAds === 'function') { 202 log(`SSSP ready after ${Date.now() - start}ms`); 203 resolve(window.sssp); 204 return; 205 } 206 207 if (Date.now() - start > timeout) { 208 reject(new Error('SSSP timeout')); 209 return; 210 } 211 212 setTimeout(check, 50); 213 } 214 215 check(); 216 }); 217 } 218 219 async function loadSeznamScripts() { 220 if (!state.isSeznamVisitor) { 221 log('Skipping script load (not Seznam visitor)'); 222 return false; 223 } 224 225 try { 226 // Load SSP script 227 await loadScript(CONFIG.SCRIPTS.ssp); 228 229 // Wait for SSSP initialization 230 await waitForSSSP(); 231 state.scriptsLoaded = true; 232 233 // Configure test mode if enabled 234 if (CONFIG.TEST_MODE && window.sssp && typeof window.sssp.config === 'function') { 235 log('Enabling TEST mode'); 236 window.sssp.config({ test: true }); 237 } 238 239 // Process pending ads 240 if (state.pendingAds.length > 0) { 241 log(`Processing ${state.pendingAds.length} pending ads`); 242 flushAds(state.pendingAds); 243 state.pendingAds = []; 244 } 245 246 return true; 247 } catch (error) { 248 console.error('[SeznamAds] Failed to load scripts:', error); 249 return false; 250 } 251 } 252 253 // ========== Ad Container Creation ========== 254 255 function createAdContainer(id, zone, screenType, extraClasses = '') { 256 const div = document.createElement('div'); 257 div.id = id; 258 div.setAttribute('data-zone', zone.zoneId); 259 div.setAttribute('data-w', zone.width); 260 div.setAttribute('data-h', zone.height); 261 div.setAttribute('data-screen-type', screenType); 262 263 const classes = ['seznam-ad']; 264 classes.push(screenType === 'mobile' ? 'mobile-only-ad' : 'de
264sktop-only-ad'); 265 if (extraClasses) { 266 classes.push(...extraClasses.split(' ')); 267 } 268 div.className = classes.join(' '); 269 270 return div; 271 } 272 273 function injectAdContainer(id, zone, screenType, targetSelector, position = 'afterend', extraClasses = '') { 274 // Check if container already exists 275 if (document.getElementById(id)) { 276 log(`Container ${id} already exists`); 277 return false; 278 } 279 280 const target = document.querySelector(targetSelector); 281 if (!target) { 282 log(`Target not found: ${targetSelector}`); 283 return false; 284 } 285 286 const container = createAdContainer(id, zone, screenType, extraClasses); 287 target.insertAdjacentElement(position, container); 288 log(`Injected ${id} ${position} ${targetSelector}`); 289 return true; 290 } 291 292 // ========== Ad Injection by Page Type ========== 293 294 function injectSinglePostAds() { 295 const isDesktop = state.screenType === 'desktop'; 296 const isMobile = state.screenType === 'mobile'; 297 298 if (isDesktop) { 299 // Desktop: Top leaderboard (pre-rendered in PHP for CLS, just verify) 300 const prerenderedLeaderboard = document.getElementById('ssp-zone-150335-post'); 301 if (!prerenderedLeaderboard) { 302 injectAdContainer( 303 'ssp-zone-150335-post', 304 AD_ZONES.desktopLeaderboard, 305 'desktop', 306 '.wp-site-blocks', 307 'beforebegin', 308 'seznam-leaderboard' 309 ); 310 } 311 312 // Desktop: After first paragraph 313 injectAdContainer( 314 'ssp-zone-215091', 315 AD_ZONES.desktopInContent, 316 'desktop', 317 '.entry-content > p:nth-of-type(1):not(:empty)' 318 ); 319 320 // Desktop: After content 321 injectAdContainer( 322 'ssp-zone-150350', 323 AD_ZONES.desktopAfterContent, 324 'desktop', 325 '.entry-content' 326 ); 327 328 // Desktop: Rectangle before footer 329 injectAdContainer( 330 'ssp-zone-150355-post', 331 AD_ZONES.desktopFooterRectangle, 332 'desktop', 333 '.wp-site-blocks > footer', 334 'beforebegin', 335 'seznam-footer-ad' 336 ); 337 } 338 339 if (isMobile) { 340 // Mobile: After first paragraph 341 injectAdContainer( 342 'ssp-zone-150360', 343 AD_ZONES.mobileFirstPara, 344 'mobile', 345 '.entry-content > p:nth-of-type(1):not(:empty)' 346 ); 347 348 // Mobile: Repeating ads every 4th paragraph 349 const paragraphs = document.querySelectorAll('.entry-content > p'); 350 let paraCounter = 0; 351 paragraphs.forEach((para) => { 352 if (!para.innerText || para.innerText.trim() === '') return; 353 paraCounter++; 354 if (paraCounter > 1 && paraCounter % 4 === 0) { 355 const adId = `ssp-zone-mobile-post-${paraCounter}`; 356 if (!document.getElementById(adId)) { 357 const container = createAdContainer(adId, AD_ZONES.mobileRepeating, 'mobile'); 358 para.insertAdjacentElement('afterend', container); 359 log(`Injected repeating ad after paragraph ${paraCounter}`); 360 } 361 } 362 }); 363 364 // Mobile: After content 365 injectAdContainer( 366 'ssp-zone-150365', 367 AD_ZONES.mobileAfterContent, 368 'mobile', 369 '.entry-content' 370 ); 371 372 // Mobile: Before footer (po ÄtÄte dále) 373 injectAdContainer( 374 'ssp-zone-150370-po-ctete-dale', 375 AD_ZONES.mobileFooter, 376 'mobile', 377 '.single .wp-site-blocks > footer, .wp-site-blocks > footer', 378 'beforebegin' 379 ); 380 381 // Mobile: Initialize pop-up reload 382 initMobilePopup(); 383 } 384 } 385 386 function injectArchiveAds() { 387 const isDesktop = state.screenType === 'desktop'; 388 const isMobile = state.screenType === 'mobile'; 389 390 // Find first query item (works for home, archive, category, search, tag) 391 const queryItemSelector = [ 392 '.home .wp-block-query', 393 '.archive #query > .query-item:first-child', 394 '.tag #query > .query-item:first-child', 395 '.category #query > .query-item:first-child', 396 '.search #query > .query-item:first-child', 397 '#query > .query-item:first-child' 398 ].join(', '); 399 400 if (isDesktop) { 401 // Desktop: Top leaderboard (pre-rendered in PHP for CLS, just verify) 402 const prerenderedLeaderboard = document.getElementById('ssp-zone-150335'); 403 if (!prerenderedLeaderboard) { 404 injectAdContainer( 405 'ssp-zone-150335', 406 AD_ZONES.desktopLeaderboard, 407 'desktop', 408 '.wp-site-blocks', 409 'beforebegin', 410 'seznam-leaderboard' 411 ); 412 } 413 414 // Desktop: After first article (wallpaper) 415 injectAdContainer( 416 'ssp-zone-215091-archive', 417 AD_ZONES.desktopInContent, 418 'desktop', 419 queryItemSelector 420 ); 421 422 // Desktop: Rectangle before footer 423 injectAdContainer( 424 'ssp-zone-150355', 425 AD_ZONES.desktopFooterRectangle, 426 'desktop', 427 '.wp-site-blocks > footer', 428 'beforebegin', 429 'seznam-footer-ad' 430 ); 431 } 432 433 if (isMobile) { 434 // Mobile: After first article 435 injectAdContainer( 436 'ssp-zone-150360-archive', 437 AD_ZONES.mobileFirstPara, 438 'mobile', 439 queryItemSelector 440 ); 441 442 // Mobile: Footer 443 injectAdContainer( 444 'ssp-zone-150370', 445 AD_ZONES.mobileFooter, 446 'mobile', 447 '.wp-site-blocks > footer', 448 'beforebegin', 449 'seznam-footer-ad' 450 ); 451 452 // Setup MutationObserver for infinite scroll 453 setupArchiveMutationObserver(); 454 } 455 } 456 457 function injectAdsForPageType() { 458 if (!state.isSeznamVisitor) { 459 log('Skipping ad injection (not Seznam visitor)'); 460 return; 461 } 462 463 log(`Injecting ads for page type: ${state.pageType}, screen: ${state.screenType}`); 464 465 if (state.pageType === 'single') { 466 injectSinglePostAds(); 467 } else if (['home', 'archive', 'category', 'search'].includes(state.pageType)) { 468 injectArchiveAds(); 469 } 470 } 471 472 // ========== Mobile Pop-up ========== 473 474 function initMobilePopup() { 475 if (state.screenType !== 'mobile') return; 476 if (state.pageType !== 'single') return; 477 478 if (!state.scriptsLoaded || !window.sssp || typeof window.sssp.getAds !== 'function') { 479 log('Pop-up skipped - SSSP not ready'); 480 return; 481 } 482 483 const zone = AD_ZONES.mobilePopupReload; 484 const containerId = `ssp-zone-${zone.zoneId}`; 485 486 // Create the pop-up container if it doesn't exist yet 487 if (!document.getElementById(containerId)) { 488 const container = document.createElement('div'); 489 container.id = containerId; 490 container.className = 'seznam-ad seznam-popup mobile-only-ad'; 491 document.body.appendChild(container); 492 } 493 494 // Single request only - no reloading (reloadCount stays 0) 495 const adObj = { 496 zoneId: zone.zoneId, 497 id: containerId, 498 width: zone.width, 499 height: zone.height, 500 options: { 501 reloadCount: 0 502 } 503 }; 504 505 log('Requesting mobile pop-up', adObj); 506 window.sssp.getAds([adObj]); 507 } 508 509 // ========== Archive MutationObserver ========== 510 511 let archiveAdCounter = 0; 512 let mutationDebounce = null; 513 514 function setupArchiveMutationObserver() { 515 const queryContainer = document.getElementById('query'); 516 if (!queryContainer) { 517 log('Query container not found for MutationObserver'); 518 return; 519 } 520 521 log('Setting up MutationObserver for infinite scroll'); 522 523 const observer = new MutationObserver((mutations) => { 524 for (const mutation of mutations) { 525 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 526 // Debounce to avoid multiple calls 527 if (mutationDebounce) { 528 clearTimeout(mutationDebounce); 529 } 530 531 mutationDebounce = setTimeout(() => { 532 log('New posts detected via MutationObserver'); 533 injectAdsIntoNewPosts(mutation.addedNodes); 534 }, 300); 535 } 536 } 537 }); 538 539 observer.observe(queryContainer, { 540 childList: true, 541 subtree: false 542 }); 543 } 544 545 function injectAdsIntoNewPosts(nodes) { 546 if (!state.isSeznamVisitor) return; 547 548 // Count valid query items added 549 let newItemCount = 0;
550 nodes.forEach(node => { 551 if (node.nodeType === 1 && node.classList && node.classList.contains('query-item')) { 552 newItemCount++; 553 archiveAdCounter++; 554 555 // Inject ad every 4th post 556 if (archiveAdCounter % 4 === 0) { 557 const adId = `ssp-zone-mobile-archive-${archiveAdCounter}`; 558 const container = createAdContainer(adId, AD_ZONES.mobileRepeating, 'mobile'); 559 node.insertAdjacentElement('afterend', container); 560 log(`Injected archive ad after item ${archiveAdCounter}`); 561 } 562 } 563 }); 564 565 // Scan for new ads and request them 566 if (newItemCount > 0) { 567 setTimeout(() => scanForAds(), 100); 568 } 569 } 570 571 // ========== Ad Request Management ========== 572 573 let adsBuffer = []; 574 let flushTimeout = null; 575 576 function flushAds(adsToRequest) { 577 if (!state.isSeznamVisitor || adsToRequest.length === 0) return; 578 579 log(`Flushing ${adsToRequest.length} ads`, adsToRequest); 580 581 if (state.scriptsLoaded && window.sssp && typeof window.sssp.getAds === 'function') { 582 window.sssp.getAds(adsToRequest); 583 } else { 584 log('Scripts not ready, queuing ads'); 585 state.pendingAds.push(...adsToRequest); 586 } 587 } 588 589 function pushAd(ad) { 590 adsBuffer.push(ad); 591 592 if (flushTimeout) { 593 clearTimeout(flushTimeout); 594 } 595 596 // Debounce requests by 250ms 597 flushTimeout = setTimeout(() => { 598 const adsToProcess = [...adsBuffer]; 599 adsBuffer = []; 600 flushAds(adsToProcess); 601 }, 250); 602 } 603 604 function loadSeznamAdById(id, zoneId = false) { 605 const ad = document.getElementById(id); 606 if (!ad) { 607 log(`Ad container not found: ${id}`); 608 return; 609 } 610 611 // Avoid double-loading 612 if (ad.getAttribute('data-ad-queued') === 'true') { 613 log(`Ad ${id} already queued`); 614 return; 615 } 616 617 // Check screen type match 618 const targetScreenType = ad.getAttribute('data-screen-type'); 619 if (targetScreenType && targetScreenType !== state.screenType) { 620 log(`Skipping ad ${id} - wrong screen type`); 621 return; 622 } 623 624 const adObj = { 625 zoneId: zoneId || ad.getAttribute('data-zone') || ad.id.replace('ssp-zone-', '').split('-')[0], 626 id: ad.getAttribute('id'), 627 width: parseInt(ad.getAttribute('data-w'), 10), 628 height: parseInt(ad.getAttribute('data-h'), 10) 629 }; 630 631 ad.setAttribute('data-ad-queued', 'true'); 632 pushAd(adObj); 633 } 634 635 function scanForAds() { 636 if (!state.isSeznamVisitor) return; 637 638 const ads = document.querySelectorAll('.seznam-ad:not([data-ad-queued="true"])'); 639 log(`Scanning: found ${ads.length} new ads`); 640 641 ads.forEach(ad => { 642 const id = ad.getAttribute('id'); 643 const zoneId = ad.getAttribute('data-zone'); 644 const targetScreenType = ad.getAttribute('data-screen-type'); 645 646 // Skip ads for wrong screen type 647 if (targetScreenType && targetScreenType !== state.screenType) { 648 return; 649 } 650 651 loadSeznamAdById(id, zoneId); 652 }); 653 } 654 655 // ========== Body Class Management ========== 656 657 function updateBodyClass() { 658 if (state.isSeznamVisitor) { 659 document.body.classList.remove('non-seznam-traffic'); 660 document.body.classList.add('seznam-traffic'); 661 } else { 662 document.body.classList.add('non-seznam-traffic'); 663 document.body.classList.remove('seznam-traffic'); 664 } 665 } 666 667 // ========== Initialization ========== 668 669 function init() { 670 if (state.initialized) return; 671 state.initialized = true; 672 673 // Detect visitor and page context 674 state.isSeznamVisitor = detectSeznamVisitor(); 675 state.pageType = detectPageType(); 676 state.screenType = detectScreenType(); 677 678 log(`Init: visitor=${state.isSeznamVisitor}, page=${state.pageType}, screen=${state.screenType}`); 679 680 // Update body class 681 updateBodyClass(); 682 683 // Expose public API 684 window.ad_manager = window.ad_manager || {}; 685 window.ad_manager.loadSeznamAdById = loadSeznamAdById; 686 window.ad_manager.scanForAds = scanForAds; 687 window.ad_manager.loadAjaxPostsAds = scanForAds; 688 window.ad_manager.screenType = state.screenType; 689 window.ad_manager.isSeznamVisitor = state.isSeznamVisitor; 690 691 if (!state.isSeznamVisitor) { 692 log('Not Seznam visitor - ads will not load'); 693 return; 694 } 695 696 // Load Seznam scripts 697 loadSeznamScripts().then(() => { 698 // Inject ad containers for page type 699 injectAdsForPageType(); 700 701 // Scan for any ads (including pre-rendered ones) 702 setTimeout(() => scanForAds(), 100); 703 }); 704 } 705 706 // Start initialization when DOM is ready 707 if (document.readyState === 'loading') { 708 document.addEventListener('DOMContentLoaded', init); 709 } else { 710 init(); 711 } 712 713})();
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.