/** * ads.js — Ghost Blog Ad Injector * Supports conditional loading, responsive ad selection, slot-based placement, * Google Analytics 4 impression tracking, and cookie-persisted test mode. */ (function () { "use strict"; // ─── Cookie Helpers ───────────────────────────────────────────────────────── function setCookie(name, value) { // Session-scoped (no expires), path=/ so it works across all pages document.cookie = name + "=" + value + ";path=/;SameSite=Lax"; } function getCookie(name) { const match = document.cookie.match( new RegExp("(?:^|;\\s*)" + name + "=([^;]*)"), ); return match ? match[1] : null; } function deleteCookie(name) { document.cookie = name + "=;path=/;SameSite=Lax;expires=Thu, 01 Jan 1970 00:00:00 GMT"; } // ─── Ad Gate ──────────────────────────────────────────────────────────────── /** * Determine whether ads should load, and manage the show_ads cookie: * ?show_ads=true → set cookie, load ads * ?show_ads=false → delete cookie, skip ads * cookie present → load ads (persisted from a previous ?show_ads=true) * March 2026 → load ads (date-based rule, no cookie interaction) */ function shouldLoadAds() { const params = new URLSearchParams(window.location.search); const paramValue = params.get("show_ads"); if (paramValue === "true") { setCookie("show_ads", "true"); return true; } if (paramValue === "false") { deleteCookie("show_ads"); return false; } if (getCookie("show_ads") === "true") { return true; } const now = new Date(); if (now.getFullYear() === 2026 && now.getMonth() === 2) return true; // 0-indexed; 2 = March return false; } /** * Returns true if ads are being forced via the show_ads cookie/param * (as opposed to the date-based rule), so we know when to show the indicator. */ function isForced() { const params = new URLSearchParams(window.location.search); return ( params.get("show_ads") === "true" || getCookie("show_ads") === "true" ); } // ─── Force Indicator ──────────────────────────────────────────────────────── /** * Display a small badge in the top-left corner of the page so testers * know ads are being force-shown via the cookie. */ function showForceIndicator() { const badge = document.createElement("div"); badge.textContent = "📢 Ads: forced"; badge.style.cssText = [ "position:fixed", "top:8px", "left:8px", "z-index:99999", "background:rgba(220,38,38,0.9)", "color:#fff", "font:bold 12px/1 system-ui,sans-serif", "padding:5px 9px", "border-radius:4px", "box-shadow:0 2px 6px rgba(0,0,0,0.35)", "pointer-events:none", "letter-spacing:0.03em", ].join(";"); document.body.appendChild(badge); } // ─── Helpers ───────────────────────────────────────────────────────────────── function isHomePage() { return window.location.pathname === "/"; } function getTimestamp() { return Date.now(); } /** Replace [timestamp] placeholder in ad src URLs */ function resolveUrl(url) { return url.replace(/\[timestamp\]/g, getTimestamp()); } /** Parse "WIDTHxHEIGHT" into { w, h } */ function parseSize(sizeStr) { const [w, h] = sizeStr.split("x").map(Number); return { w, h }; } /** * Fire a GA4 event for an ad impression. * Requires gtag() to be available on the page (standard GA4 snippet). */ function trackImpression(adDef) { if (typeof window.gtag !== "function") { console.warn( "[GhostAds] gtag() not found — skipping impression tracking for:", adDef.slot, ); return; } window.gtag("event", "ad_impression", { advertiser: adDef.advertiser, ad_size: adDef.size, ad_slot: adDef.slot, }); } /** * Given a list of ad definitions assigned to a slot and the current viewport width, * pick the best fitting ad whose width <= viewport width. * Prefers the largest ad that fits. */ function pickBestAd(ads, viewportWidth) { const fitting = ads .map((ad) => ({ ad, size: parseSize(ad.size) })) .filter(({ size }) => size.w <= viewportWidth) .sort((a, b) => b.size.w - a.size.w); // largest first return fitting.length ? fitting[0].ad : null; } /** * Build the wrapper div + iframe for an ad definition. * Ad def shape: { advertiser, size, slot, iframeSrc, scriptSrc } */ function buildAdElement(adDef) { const { w, h } = parseSize(adDef.size); const iframeSrc = resolveUrl(adDef.iframeSrc); const scriptSrc = resolveUrl(adDef.scriptSrc); const wrapper = document.createElement("div"); wrapper.setAttribute("data-advertiser", adDef.advertiser); wrapper.setAttribute("data-ad-size", adDef.size); wrapper.setAttribute("data-ad-slot", adDef.slot); wrapper.style.cssText = "width:100%;text-align:center;overflow:hidden;margin:16px 0;"; const iframe = document.createElement("iframe"); iframe.src = iframeSrc; iframe.width = w; iframe.height = h; iframe.marginWidth = 0; iframe.marginHeight = 0; iframe.hspace = 0; iframe.vspace = 0; iframe.frameBorder = 0; iframe.scrolling = "no"; iframe.style.cssText = "display:block;margin:0 auto;border:none;"; iframe.setAttribute("bordercolor", "#000000"); const scriptTag = `