/** * Ad Widget Hydration for MkDocs * * Converts ad block placeholders (inserted via DocsPage toolbar) into * rendered ad cards when pages are viewed in MkDocs. * * Supports: * - .ad-specific-block[data-ad-id] — renders a specific ad by ID * - .ad-slot-block[data-placement][data-variant] — renders a dynamic ad slot * * Reads API URL from window.PAYMENT_API_URL (set by env-config.js). * Tracks impressions and clicks via POST /api/gallery-ads/track. */ (function () { 'use strict'; var API_URL = window.PAYMENT_API_URL || ''; /** Lighten/darken a hex color by an amount */ function adjustColor(hex, amount) { function clamp(v) { return Math.max(0, Math.min(255, v)); } var h = hex.replace('#', ''); var r = clamp(parseInt(h.substring(0, 2), 16) + amount); var g = clamp(parseInt(h.substring(2, 4), 16) + amount); var b = clamp(parseInt(h.substring(4, 6), 16) + amount); return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0'); } /** Get or create a simple session ID for tracking */ function getSessionId() { var key = 'cm_ad_session'; var id = sessionStorage.getItem(key); if (!id) { id = Math.random().toString(36).slice(2) + Date.now().toString(36); sessionStorage.setItem(key, id); } return id; } /** Track an impression or click */ function trackEvent(adId, event) { if (!API_URL) return; fetch(API_URL + '/api/gallery-ads/track', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ adId: adId, event: event, sessionId: getSessionId() }), }).catch(function () {}); // silent } /** Render an ad card into a container element */ function renderAdCard(container, ad) { container.innerHTML = ''; container.setAttribute('data-hydrated', 'true'); var isHighlight = ad.variant === 'highlight'; var isMinimal = ad.variant === 'minimal'; var defaultPrimary = '#1677ff'; var bgColor = ad.bgColor || defaultPrimary; var bgGradient = ad.bgColor ? 'linear-gradient(135deg, ' + ad.bgColor + ' 0%, ' + adjustColor(ad.bgColor, -30) + ' 100%)' : isHighlight ? 'linear-gradient(135deg, ' + defaultPrimary + ' 0%, ' + adjustColor(defaultPrimary, -40) + ' 100%)' : 'linear-gradient(135deg, #1f1f2e 0%, #141422 100%)'; var borderStyle = isHighlight ? '2px solid ' + bgColor : '1px solid rgba(255,255,255,0.08)'; var card = document.createElement('div'); card.style.cssText = 'border-radius:12px;overflow:hidden;border:' + borderStyle + ';cursor:' + (ad.linkUrl ? 'pointer' : 'default') + ';transition:all 0.2s ease;display:flex;flex-direction:column;max-width:400px;margin:16px auto;' + (isHighlight ? 'box-shadow:0 0 20px ' + bgColor + '33;' : ''); // Click handler card.addEventListener('click', function () { trackEvent(ad.id, 'click'); if (ad.linkUrl) { if (ad.linkUrl.indexOf('http') === 0) { window.open(ad.linkUrl, '_blank', 'noopener'); } else { window.location.href = ad.linkUrl; } } }); // Top section (16:9 visual area) — skip for minimal variant if (!isMinimal) { var topBg = ad.imagePath ? 'url(' + ad.imagePath + ') center/cover no-repeat' : bgGradient; var top = document.createElement('div'); top.style.cssText = 'position:relative;padding-top:56.25%;background:' + topBg + ';'; var overlay = document.createElement('div'); overlay.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;text-align:center;' + (ad.imagePath ? 'background:rgba(0,0,0,0.5);' : ''); if (ad.iconEmoji) { var emoji = document.createElement('span'); emoji.style.cssText = 'font-size:36px;margin-bottom:8px;'; emoji.textContent = ad.iconEmoji; overlay.appendChild(emoji); } var title = document.createElement('h4'); title.style.cssText = 'color:#fff;margin:0;text-shadow:0 1px 3px rgba(0,0,0,0.3);font-size:16px;font-weight:600;'; title.textContent = ad.title; overlay.appendChild(title); if (ad.subtitle) { var subtitle = document.createElement('p'); subtitle.style.cssText = 'color:rgba(255,255,255,0.85);margin:8px 0 0;font-size:13px;max-width:240px;'; subtitle.textContent = ad.subtitle; overlay.appendChild(subtitle); } top.appendChild(overlay); card.appendChild(top); } // Bottom section var bottom = document.createElement('div'); bottom.style.cssText = 'padding:' + (isMinimal ? '20px 16px' : '12px 16px') + ';background:' + (isMinimal ? bgGradient : '#1b2838') + ';display:flex;flex-direction:column;gap:8px;'; if (isMinimal) { if (ad.iconEmoji) { var emojiMin = document.createElement('span'); emojiMin.style.fontSize = '24px'; emojiMin.textContent = ad.iconEmoji; bottom.appendChild(emojiMin); } var titleMin = document.createElement('h5'); titleMin.style.cssText = 'color:#fff;margin:0;font-size:14px;font-weight:600;'; titleMin.textContent = ad.title; bottom.appendChild(titleMin); if (ad.subtitle) { var subMin = document.createElement('p'); subMin.style.cssText = 'color:rgba(255,255,255,0.6);font-size:12px;margin:0;'; subMin.textContent = ad.subtitle; bottom.appendChild(subMin); } } if (ad.ctaText) { var cta = document.createElement('a'); cta.textContent = ad.ctaText; cta.href = ad.linkUrl || '#'; cta.style.cssText = 'display:block;text-align:center;padding:6px 16px;border-radius:6px;font-size:13px;font-weight:500;text-decoration:none;' + (ad.ctaStyle === 'primary' ? 'background:' + defaultPrimary + ';color:#fff;' : ad.ctaStyle === 'outline' ? 'background:transparent;color:' + defaultPrimary + ';border:1px solid ' + defaultPrimary + ';' : 'background:transparent;color:' + defaultPrimary + ';'); cta.addEventListener('click', function (e) { e.stopPropagation(); }); bottom.appendChild(cta); } var promo = document.createElement('p'); promo.style.cssText = 'font-size:10px;color:rgba(255,255,255,0.25);text-align:center;margin:0;'; promo.textContent = 'Promoted'; bottom.appendChild(promo); card.appendChild(bottom); container.appendChild(card); // Impression tracking via IntersectionObserver var impressionSent = false; var timer = null; var observer = new IntersectionObserver(function (entries) { if (entries[0] && entries[0].isIntersecting) { timer = setTimeout(function () { if (!impressionSent) { impressionSent = true; trackEvent(ad.id, 'impression'); } }, 1000); } else if (timer) { clearTimeout(timer); timer = null; } }, { threshold: 0.5 }); observer.observe(card); } /** Hydrate all ad blocks on the page */ function hydrateAds() { if (!API_URL) return; var specificBlocks = document.querySelectorAll('.ad-specific-block:not([data-hydrated])'); var slotBlocks = document.querySelectorAll('.ad-slot-block:not([data-hydrated])'); if (specificBlocks.length === 0 && slotBlocks.length === 0) return; // Fetch all active ads (for both specific and slot hydration) fetch(API_URL + '/api/gallery-ads?placement=docs') .then(function (r) { return r.json(); }) .then(function (ads) { if (!Array.isArray(ads)) return; // Hydrate specific ad blocks specificBlocks.forEach(function (el) { var adId = parseInt(el.getAttribute('data-ad-id') || '0', 10); if (!adId) return; var ad = ads.find(function (a) { return a.id === adId; }); if (!ad) { // Ad not found or not active — try fetching all ads (placement filter may exclude it) fetch(API_URL + '/api/gallery-ads') .then(function (r) { return r.json(); }) .then(function (allAds) { var found = allAds.find(function (a) { return a.id === adId; }); if (found) renderAdCard(el, found); else el.style.display = 'none'; }) .catch(function () { el.style.display = 'none'; }); return; } renderAdCard(el, ad); }); // Hydrate dynamic ad slot blocks if (slotBlocks.length > 0 && ads.length > 0) { var idx = 0; slotBlocks.forEach(function (el) { var variant = el.getAttribute('data-variant') || 'standard'; var match = ads.find(function (a) { return a.variant === variant; }) || ads[idx % ads.length]; idx++; if (match) renderAdCard(el, match); else el.style.display = 'none'; }); } else if (slotBlocks.length > 0) { slotBlocks.forEach(function (el) { el.style.display = 'none'; }); } }) .catch(function (err) { console.warn('[Ad Widgets] Failed to fetch ads:', err); specificBlocks.forEach(function (el) { el.style.display = 'none'; }); slotBlocks.forEach(function (el) { el.style.display = 'none'; }); }); } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hydrateAds); } else { hydrateAds(); } // Re-initialize on MkDocs SPA navigation if (typeof window.document$ !== 'undefined') { window.document$.subscribe(function () { setTimeout(hydrateAds, 100); }); } })();