universal-ad-loader 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ (()=>{function ce(){let e={};return{on(r,t){return(e[r]=e[r]||[]).push(t),()=>this.off(r,t)},off(r,t){e[r]=(e[r]||[]).filter(o=>o!==t)},emit(r,t){[...e[r]||[],...e["*"]||[]].forEach(o=>{try{o(t,r)}catch(i){console.error("[UAL] handler error",r,i)}})}}}var de="[UAL]",F=!1;try{F=/[?&]ual_debug=1/.test(location.search)||localStorage.getItem("ual_debug")==="1"}catch(e){}function Y(e){F=!!e}function ue(){return F}function I(...e){F&&console.log(de,...e)}function S(...e){console.warn(de,...e)}function $(e,r={}){return new Promise((t,o)=>{let i=document.querySelector(`script[data-ual-src="${e}"]`);if(i){if(i.dataset.loaded)return t();i.addEventListener("load",()=>t()),i.addEventListener("error",o);return}let a=document.createElement("script");a.async=!0,a.src=e,a.dataset.ualSrc=e,Object.entries(r).forEach(([c,l])=>a.setAttribute(c,l)),a.onload=()=>{a.dataset.loaded="1",t()},a.onerror=()=>o(new Error("Script load failed: "+e)),document.head.appendChild(a)})}function fe(e,r){return e.replace(/\{([\w.]+)\}/g,(t,o)=>{let i=o.split(".").reduce((a,c)=>a==null?void 0:a[c],r);return i==null?"":encodeURIComponent(String(i))})}function q(e){(e||[]).forEach(r=>{if(!r)return;try{if(navigator.sendBeacon&&navigator.sendBeacon(r))return}catch(o){}let t=new Image(1,1);t.src=r})}function R(e,r,t){let o;return Promise.race([e,new Promise(i=>{o=setTimeout(()=>i(t),r)})]).finally(()=>clearTimeout(o))}function W(){return Math.floor(Math.random()*1e10)}function pe(){let e=window.innerWidth||document.documentElement.clientWidth;return e<768?"mobile":e<1024?"tablet":"desktop"}var tt={site:null,consent:{mode:"tcf",timeoutMs:4e3,requiredPurposes:[1]},lazy:{enabled:!0,fetchMargin:"200px"},viewability:{threshold:.5,durationMs:1e3},collapseEmpty:!0,providerTimeoutMs:3e3,targeting:{},providers:{},slots:{},pageRules:[]};async function ge(e){let r=e;if(typeof e=="string"){let t=await fetch(e,{credentials:"omit"});if(!t.ok)throw new Error("Config HTTP "+t.status);r=await t.json()}return we(tt,r||{})}var me=["optional","required","contextual"];function he(e,r){let t=o=>S("Config: "+o);e.site||t('manca "site"');for(let[o,i]of Object.entries(e.providers||{})){if(!i||!i.adapter){t(`provider "${o}" senza "adapter"`);continue}r&&!r.has(i.adapter)&&t(`provider "${o}": adapter "${i.adapter}" non registrato (registralo con registerAdapter prima dell'init)`),i.consent&&!me.includes(i.consent)&&t(`provider "${o}": consent "${i.consent}" non valido (${me.join("|")})`)}for(let[o,i]of Object.entries(e.slots||{})){if(!i||!Array.isArray(i.providers)||!i.providers.length){t(`slot "${o}" senza "providers"`);continue}i.providers.forEach(a=>{let c=typeof a=="string"?a:a&&a.id;(!c||!e.providers[c])&&t(`slot "${o}": provider "${c}" non definito in "providers"`)})}}function we(e,r){let t={...e};for(let o of Object.keys(r)){let i=r[o];t[o]=i&&typeof i=="object"&&!Array.isArray(i)&&e[o]&&typeof e[o]=="object"&&!Array.isArray(e[o])?we(e[o],i):i}return t}function ye(e,r=location.pathname){let t={...e,slots:JSON.parse(JSON.stringify(e.slots)),targeting:{...e.targeting}};for(let o of e.pageRules||[]){let i;try{i=new RegExp(o.match)}catch(a){S("pageRule regex non valida",o.match);continue}i.test(r)&&((o.disable||[]).forEach(a=>{t.slots[a]&&(t.slots[a].enabled=!1)}),(o.enable||[]).forEach(a=>{t.slots[a]&&(t.slots[a].enabled=!0)}),Object.assign(t.targeting,o.targeting||{}),Object.entries(o.slots||{}).forEach(([a,c])=>{t.slots[a]={...t.slots[a],...c}}))}return t}function ve(e){let r=window.innerWidth||document.documentElement.clientWidth;if(Array.isArray(e.sizeMapping)&&e.sizeMapping.length){let t=[...e.sizeMapping].sort((o,i)=>i.minWidth-o.minWidth).find(o=>r>=o.minWidth);return t?t.sizes:[]}return e.sizes||[]}function be(e){return new Promise(r=>{if(typeof window.__tcfapi=="function")return r(!0);let t=Date.now(),o=setInterval(()=>{typeof window.__tcfapi=="function"?(clearInterval(o),r(!0)):Date.now()-t>e&&(clearInterval(o),r(!1))},50)})}function Se(e,r){window.__tcfapi("addEventListener",2,(t,o)=>{!o||!t||(t.gdprApplies===!1||t.eventStatus==="tcloaded"||t.eventStatus==="useractioncomplete")&&r(nt(t,e))})}var Ee=Symbol("timeout");async function Ae(e={},r){let t={mode:"tcf",timeoutMs:4e3,cmpWaitMs:1500,lateWatchMs:3e4,requiredPurposes:[1],vendorId:null,...e};if(t.mode==="none")return{gdprApplies:!1,tcString:"",purposes:{},granted:!0,source:"none"};if(!await be(t.cmpWaitMs))return I("Nessuna CMP TCF trovata (continuo a cercarla)"),r&&be(t.lateWatchMs).then(l=>{l&&Se(t,r)}),{gdprApplies:void 0,tcString:"",purposes:{},granted:!1,source:"no-cmp"};let i=!1,a=new Promise(l=>{Se(t,u=>{i?r&&r(u):(i=!0,l(u))})}),c=await R(a,t.timeoutMs,Ee);return i=!0,c===Ee?{gdprApplies:!0,tcString:"",purposes:{},granted:!1,source:"timeout"}:c}function nt(e,r){let t=e.purpose&&e.purpose.consents||{},o=e.vendor&&e.vendor.consents||{},i=e.gdprApplies===!1||r.requiredPurposes.every(c=>t[c]===!0);i&&e.gdprApplies!==!1&&r.vendorId&&(i=o[r.vendorId]===!0);let a={gdprApplies:e.gdprApplies,tcString:e.tcString||"",purposes:t,vendorConsents:o,granted:i,source:"tcf"};return I("Consenso",a.granted?"OK":"limitato",a),a}var xe="allow-scripts allow-popups allow-popups-to-escape-sandbox",B=2e3,rt=0,te=new Map;function Te(e){let r=Math.round(Number(e));return r>0&&r<=B?r:0}function H(e,r){if(typeof e!="string"||!e)return null;try{let t=new URL(e,location.href);if(t.protocol==="http:"||t.protocol==="https:"||r&&t.protocol==="data:"&&/^data:image\//i.test(t.href))return t.href}catch(t){}return null}function Z(e){return(Array.isArray(e)?e:[]).map(r=>H(r)).filter(Boolean)}function ze(e){if(!e||typeof e!="object")return{ok:!1,reason:"annuncio non valido"};let r=e.type||(e.url?"url":e.src?"image":"html"),t=Te(e.width),o=Te(e.height);if(!t||!o)return{ok:!1,reason:`dimensioni non valide (${e.width}x${e.height}, max ${B})`};let i=e.trackers||{},a={type:r,width:t,height:o,alt:e.alt==null?void 0:String(e.alt),clickUrl:H(e.clickUrl)||"",trackers:{impression:Z(i.impression),viewable:Z(i.viewable),click:Z(i.click)},sandbox:typeof e.sandbox=="string"?e.sandbox:void 0};if(r==="image"){if(a.src=H(e.src,!0),!a.src)return{ok:!1,reason:"src immagine non valido (ammessi http, https, data:image)"}}else if(r==="url"){if(a.url=H(e.url),!a.url)return{ok:!1,reason:"url non valido (ammessi solo http e https)"}}else if(r==="html"){if(typeof e.html!="string"||!e.html)return{ok:!1,reason:"html mancante"};a.html=e.html}else return{ok:!1,reason:"tipo sconosciuto: "+r};return{ok:!0,ad:a}}function Ie(e){return String(e).split(/\s+/).filter(Boolean)}function it(e){let r=e.sandbox&&e.sandbox!=="none"?e.sandbox:null;if(e.type==="url"){let t=!0;try{t=new URL(e.url).origin===location.origin}catch(o){}return e.sandbox==="none"&&!t?null:Ie(r||xe+" allow-same-origin").filter(o=>o!=="allow-top-navigation"&&!(t&&o==="allow-same-origin")).join(" ")}return Ie(r||xe).filter(t=>t!=="allow-top-navigation"&&t!=="allow-same-origin").join(" ")}function ke(e){return JSON.stringify(String(e)).replace(/</g,"\\u003c").replace(/>/g,"\\u003e").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029")}function ot(e,r){return`<script>(function(){
2
+ var C=${ke(e)};
3
+ function s(m){m.ual=C;parent.postMessage(m,'*');}
4
+ window.clickTag=${ke(r||"")};
5
+ window.UALExt={
6
+ resize:function(w,h){s({cmd:'resize',w:w,h:h});},
7
+ expand:function(w,h){s({cmd:'resize',w:w,h:h,expanded:true});},
8
+ collapse:function(){s({cmd:'collapse'});},
9
+ close:function(){s({cmd:'close'});},
10
+ click:function(){s({cmd:'click'});if(window.clickTag)window.open(window.clickTag,'_blank');}
11
+ };
12
+ })();<\/script>`}function ee(e){return String(e).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function st(e,r){let t='<!doctype html><html><head><meta charset="utf-8"><base target="_blank"><style>html,body{margin:0;padding:0;overflow:hidden;background:transparent}img{display:block;border:0;max-width:100%}</style>'+ot(r,e.clickUrl)+"</head><body>",o="";if(e.type==="image"){let i=`<img src="${ee(e.src)}" width="${e.width}" height="${e.height}" alt="${ee(e.alt||"Advertisement")}">`;o=e.clickUrl?`<a href="${ee(e.clickUrl)}" rel="noopener sponsored" onclick="parent.postMessage({ual:'${r}',cmd:'click'},'*')">${i}</a>`:i}else o=e.html||"";return t+o+"</body></html>"}function Ce(e,r,{slotId:t,onClick:o,onClose:i}={}){at();let a="ual"+ ++rt,c=document.createElement("iframe");c.title="Advertisement",c.setAttribute("scrolling","no"),c.setAttribute("frameborder","0"),c.setAttribute("loading","eager"),c.setAttribute("referrerpolicy","strict-origin-when-cross-origin"),c.style.cssText=`border:0;display:block;margin:0 auto;width:${r.width}px;height:${r.height}px;max-width:100%`,c.width=r.width,c.height=r.height;let l=it(r);return l!==null&&c.setAttribute("sandbox",l),r.type==="url"?c.src=r.url:c.srcdoc=st(r,a),te.set(a,{iframe:c,slotId:t,ad:r,original:{w:r.width,h:r.height},onClick:()=>{q(r.trackers&&r.trackers.click),o&&o()},onClose:i}),e.innerHTML="",e.appendChild(c),{iframe:c,channel:a,destroy:()=>te.delete(a)}}var Me=!1;function at(){Me||(Me=!0,window.addEventListener("message",lt))}function lt(e){let r=e.data;if(!r||typeof r!="object"||!r.ual)return;let t=te.get(r.ual);if(!(!t||e.source!==t.iframe.contentWindow))switch(r.cmd){case"resize":{let o=Math.min(+r.w||t.original.w,B),i=Math.min(+r.h||t.original.h,B);t.iframe.style.width=o+"px",t.iframe.style.height=i+"px";break}case"collapse":t.iframe.style.width=t.original.w+"px",t.iframe.style.height=t.original.h+"px";break;case"close":t.onClose&&t.onClose();break;case"click":t.onClick();break;default:}}function Oe(e,{threshold:r=.5,durationMs:t=1e3}={},o){if(!("IntersectionObserver"in window))return()=>{};let i=null,a=!1,c=!1,l=()=>{a||i||document.visibilityState!=="visible"||(i=setTimeout(()=>{a=!0,E(),o()},t))},u=()=>{clearTimeout(i),i=null},f=new IntersectionObserver(A=>{A.forEach(p=>{c=p.intersectionRatio>=r,c?l():u()})},{threshold:[0,r,1]}),y=()=>document.visibilityState==="visible"&&c?l():u();document.addEventListener("visibilitychange",y),f.observe(e);function E(){u(),f.disconnect(),document.removeEventListener("visibilitychange",y)}return E}var ct=new Set(["unconfigured","disabled","closed"]),dt=242500;function $e(e){let r=ce(),t=null,o=null,i={},a={gdprApplies:void 0,tcString:"",purposes:{},granted:!1,source:"pending"},c=Promise.resolve(a),l=!1,u=null,f=new Map,y=new Map,E=0,A=null,p=null,v=new Set,M=null;function z(){return{site:t.site,page:{url:location.href,path:location.pathname,ref:document.referrer,title:document.title},device:pe(),viewport:{w:window.innerWidth,h:window.innerHeight},targeting:{...t.targeting},consent:a,debug:ue()}}function k(){let n=ye(o);return Object.assign(n.targeting,i),n}function _(n){let s=a;if(!(s.granted===n.granted&&s.tcString===n.tcString&&s.source===n.source)&&(a=n,r.emit("consent",a),!s.granted&&n.granted))for(let d of[...f.values()])d.consentSkipped&&U(d)}function U(n,s=20){if(f.get(n.instanceId)===n){if(n.status==="loading"||n.status==="queued"){s>0&&setTimeout(()=>U(n,s-1),250);return}K(n)}}function We(n){return u?(S("init gi\xE0 eseguito"),u):(u=He(n).catch(s=>{throw u=null,s}),u)}async function He(n){o=await ge(n),o.debug&&Y(!0),he(o,e),t=k(),I("Config",t),c=Ae(t.consent,_).then(s=>(_(s),s)),document.readyState==="loading"&&await new Promise(s=>document.addEventListener("DOMContentLoaded",s,{once:!0})),l=!0,Xe(),V(document),Je(),r.emit("ready",{site:t.site})}function V(n){let s=[];n.nodeType===1&&n.matches&&n.matches("[data-ad-slot]")&&s.push(n),n.querySelectorAll&&n.querySelectorAll("[data-ad-slot]").forEach(d=>s.push(d)),s.forEach(Be)}function Be(n){let s=n.dataset.ualId;if(!s)return oe(n);f.has(s)||ct.has(n.dataset.ualStatus)||(ie(n),oe(n))}function ie(n){delete n.dataset.ualId,delete n.dataset.ualStatus,n.querySelectorAll(":scope > .ual-label, :scope > .ual-creative").forEach(s=>s.remove()),n.classList.remove("ual-slot","ual-filled"),n.style.display="",n.style.minHeight=""}function oe(n){let s=n.getAttribute("data-ad-slot"),d=t.slots[s];if(n.dataset.ualId=s+"#"+ ++E,!d){I("Slot non configurato, ignorato:",s),n.dataset.ualStatus="unconfigured";return}if(d.enabled===!1){I("Slot disabilitato:",s),n.dataset.ualStatus="disabled",X(n,d);return}let w={};try{w=n.dataset.adParams?JSON.parse(n.dataset.adParams):{}}catch(T){S("data-ad-params non \xE8 JSON valido",n)}let h={instanceId:n.dataset.ualId,name:s,el:n,cfg:d,params:w,status:"idle",cleanup:[],tries:0,consentSkipped:!1};f.set(h.instanceId,h),Ve(h),r.emit("slotRegistered",C(h)),(d.lazy!==void 0?d.lazy:t.lazy.enabled)&&A?A.observe(n):J(h)}function Ve(n){let{el:s,cfg:d}=n;if(d.minHeight&&(s.style.minHeight=d.minHeight+"px"),s.classList.add("ual-slot"),d.label){let h=document.createElement("div");h.className="ual-label",h.textContent=d.label,h.style.cssText="font:10px/1.4 sans-serif;color:#888;text-align:center;text-transform:uppercase;letter-spacing:.05em",s.appendChild(h)}let w=document.createElement("div");w.className="ual-creative",s.appendChild(w),n.box=w}function Je(){"MutationObserver"in window&&(p=new MutationObserver(n=>{for(let s of n)s.removedNodes.forEach(d=>{if(d.nodeType===1)for(let w of[...f.values()])(d===w.el||d.contains(w.el))&&G(w)}),s.addedNodes.forEach(d=>{d.nodeType===1&&V(d)})}),p.observe(document.body||document.documentElement,{childList:!0,subtree:!0}))}function Xe(){"IntersectionObserver"in window&&(A=new IntersectionObserver(n=>{n.forEach(s=>{if(!s.isIntersecting)return;A.unobserve(s.target);let d=f.get(s.target.dataset.ualId);d&&J(d)})},{rootMargin:t.lazy.fetchMargin}))}function J(n){n.status!=="loading"&&(n.status="queued",v.add(n),clearTimeout(M),M=setTimeout(Ke,30))}function Ke(){let n=[...v];v=new Set,n.length&&se(n,0)}async function se(n,s){let d=new Map,w=[];for(let h of n){let b=(h.cfg.providers||[])[s];if(!b){w.push(h);continue}let T=typeof b=="string"?b:b.id;d.has(T)||d.set(T,[]),d.get(T).push(h)}w.forEach(Ye),await Promise.all([...d.entries()].map(async([h,b])=>{let T=await Qe(h,b,s);T.length&&await se(T,s+1)}))}async function Ge(n){let s=t.providers[n];if(!s)throw new Error("Provider non definito: "+n);let d=e.get(s.adapter);if(!d)throw new Error("Adapter non registrato: "+s.adapter);return y.has(n)||y.set(n,Promise.resolve(d.init?d.init(s,z()):null)),await y.get(n),{adapter:d,pcfg:s}}async function Qe(n,s,d){let w=t.providers[n];if((!w||w.consent!=="contextual")&&await c,s=s.filter(m=>f.get(m.instanceId)===m),!s.length)return[];let h,b;try{({adapter:h,pcfg:b}=await Ge(n))}catch(m){return S(m.message),r.emit("error",{provider:n,error:m}),s}if(b.consent==="required"&&!a.granted)return I(`Provider ${n} saltato: consenso non concesso`),s.forEach(m=>{m.consentSkipped=!0}),s;let T=z(),O=s.map(m=>{m.status="loading",m.provider=n;let g=m.cfg.providers[d];return{id:m.instanceId,name:m.name,el:m.box,sizes:ve(m.cfg),params:{...typeof g=="object"?g.params:{},...m.params},cfg:m.cfg}});O.forEach(m=>r.emit("slotRequested",{slot:m.name,id:m.id,provider:n})),I(`Richiesta a ${n} (${b.adapter})`,O.map(m=>m.name));let le=b.timeoutMs||t.providerTimeoutMs,L=[];try{if(h.fill){let m=await R(h.fill(O,b,T),le+2e3,{});s.forEach((g,Q)=>{let x=m[O[Q].id];x&&x.filled?ae(g,{width:x.width,height:x.height,selfRendered:!0,meta:x.meta}):(N(g),P(g),L.push(g))})}else{let m=await R(h.request(O,b,T),le,{});s.forEach((g,Q)=>{let x=m[O[Q].id];if(!x||!x.html&&!x.src&&!x.url){L.push(g);return}let D=ze(x);if(!D.ok){S(`Annuncio scartato (${n}, slot ${g.name}): ${D.reason}`),r.emit("error",{provider:n,slot:g.name,error:new Error(D.reason)}),L.push(g);return}let j=D.ad,et=Ce(g.box,j,{slotId:g.instanceId,onClick:()=>r.emit("slotClick",C(g)),onClose:()=>{G(g),g.el.dataset.ualStatus="closed",X(g.el,g.cfg,!0)}});g.cleanup.push(et.destroy),q(j.trackers.impression),ae(g,{width:j.width,height:j.height,ad:j})})}}catch(m){S(`Errore provider ${n}`,m),r.emit("error",{provider:n,error:m}),s.forEach(g=>{g.status==="loading"&&(N(g),P(g),L.push(g))})}return L}function ae(n,s){n.status="filled",n.el.dataset.ualStatus="filled",n.el.classList.add("ual-filled"),n.el.style.display="",r.emit("slotRendered",{...C(n),width:s.width,height:s.height,meta:s.meta});let d={...t.viewability,...n.cfg.viewability};(s.width||0)*(s.height||0)>dt&&!(n.cfg.viewability&&n.cfg.viewability.threshold!==void 0)&&(d.threshold=Math.min(d.threshold,.3));let h=n.box.firstElementChild||n.box,b=Oe(h,d,()=>{s.ad&&q(s.ad.trackers.viewable),r.emit("slotViewable",C(n)),Ze(n)});n.cleanup.push(b)}function Ye(n){n.status="empty",n.el.dataset.ualStatus="empty",P(n),X(n.el,n.cfg),r.emit("slotEmpty",C(n))}function X(n,s,d){(d||(s.collapseEmpty!==void 0?s.collapseEmpty:t&&t.collapseEmpty))&&(n.style.display="none",n.style.minHeight="")}function P(n){n.cleanup.forEach(s=>{try{s()}catch(d){}}),n.cleanup=[],n.box&&(n.box.innerHTML="")}function N(n){let s=n.provider&&t.providers[n.provider],d=s&&e.get(s.adapter);if(d&&d.destroy)try{d.destroy({id:n.instanceId,el:n.box},s)}catch(w){}}function Ze(n){let s=n.cfg.refreshSec;if(!s||n.cfg.maxRefresh&&n.tries>=n.cfg.maxRefresh)return;let d=()=>{n.refreshTimer=setTimeout(()=>{if(!f.has(n.instanceId))return;let w=n.el.getBoundingClientRect();w.bottom>0&&w.top<window.innerHeight&&document.visibilityState==="visible"?(n.tries++,K(n)):d()},s*1e3)};d(),n.cleanup.push(()=>clearTimeout(n.refreshTimer))}function K(n){n.status==="loading"||n.status==="queued"||(N(n),P(n),n.status="idle",n.consentSkipped=!1,J(n))}function G(n){N(n),P(n),A&&A.unobserve(n.el),f.delete(n.instanceId)}function C(n){return{slot:n.name,id:n.instanceId,provider:n.provider,status:n.status,el:n.el}}return{init:We,isInitialized:()=>u!==null,whenReady:()=>u,on:r.on.bind(r),off:r.off.bind(r),registerAdapter:(n,s)=>e.set(n,s),setTargeting(n){Object.entries(n||{}).forEach(([s,d])=>{d==null?(delete i[s],t&&delete t.targeting[s]):(i[s]=d,t&&(t.targeting[s]=d))})},refresh(n){for(let s of f.values())(!n||n.includes(s.name))&&K(s)},pageView(){if(l){for(let n of[...f.values()])G(n);document.querySelectorAll("[data-ual-id]").forEach(ie),t=k(),V(document)}},getSlots:()=>[...f.values()].map(C),getConsent:()=>a,debug:Y}}var Pe={name:"json",async request(e,r,t){let o={site:t.site,page:t.page,device:t.device,viewport:t.viewport,targeting:t.targeting,consent:{gdprApplies:t.consent.gdprApplies,tcString:t.consent.tcString,granted:t.consent.granted},slots:e.map(l=>({id:l.id,name:l.name,sizes:l.sizes,params:l.params}))},i=await fetch(r.endpoint,{method:"POST",headers:{"Content-Type":"application/json",...r.headers},body:JSON.stringify(o),credentials:r.withCredentials&&t.consent.granted?"include":"omit",keepalive:!1});if(i.status===204)return{};if(!i.ok)throw new Error("decide HTTP "+i.status);let a=await i.json();r.map&&typeof window[r.map]=="function"&&(a=window[r.map](a,e));let c={};return(a.ads||[]).forEach(l=>{l&&l.slotId&&(c[l.slotId]=l)}),c}};var Le={name:"iframe",async request(e,r,t){let o={};return e.forEach(i=>{let[a,c]=i.sizes&&i.sizes[0]||[i.params.width||300,i.params.height||250],l=fe(r.url,{params:i.params,width:a,height:c,site:t.site,slot:i.name,device:t.device,cb:W(),page:t.page,consent:{tcString:t.consent.tcString,gdpr:t.consent.gdprApplies?1:0}});o[i.id]={type:"url",url:l,width:a,height:c,sandbox:r.sandbox||void 0}}),o}};function je(e){return e.server.replace(/\/$/,"")+(e.deliveryPath||"/www/delivery")}var qe={name:"revive",async request(e,r,t){let o={};return e.forEach(i=>{if(i.params.zoneid==null){S(`revive: zoneid mancante per lo slot "${i.name}", slot saltato`);return}let[a,c]=i.sizes&&i.sizes[0]||[300,250],l=new URLSearchParams({zoneid:i.params.zoneid,cb:W()});t.page.ref&&l.set("referer",t.page.ref),Object.entries(i.params.vars||{}).forEach(([u,f])=>l.set(u,f)),o[i.id]={type:"url",url:`${je(r)}/afr.php?${l}`,width:a,height:c}}),o}},Re={name:"revive-async",async fill(e,r){let t={},o=`${je(r)}/asyncjs.php`,i=e.filter(l=>l.params.zoneid!=null?!0:(S(`revive-async: zoneid mancante per lo slot "${l.name}", slot saltato`),t[l.id]={filled:!1},!1));if(!i.length)return t;let a=i.map(l=>{let u=document.createElement("ins");return u.setAttribute("data-revive-zoneid",l.params.zoneid),u.setAttribute("data-revive-id",r.reviveId),Object.entries(l.params.vars||{}).forEach(([f,y])=>u.setAttribute("data-revive-"+f,y)),l.el.appendChild(u),new Promise(f=>{let y=p=>{E.disconnect(),clearTimeout(A);let v=u.getBoundingClientRect();t[l.id]={filled:p,width:Math.round(v.width),height:Math.round(v.height)},f()},E=new MutationObserver(()=>{u.querySelector("img,iframe,a,div,embed")&&y(!0)});E.observe(u,{childList:!0,subtree:!0});let A=setTimeout(()=>y(u.children.length>0),r.fillTimeoutMs||2500)})}),c=window.reviveAsync&&window.reviveAsync[r.reviveId];return c&&typeof c.refresh=="function"?c.refresh():await $(o),await Promise.all(a),t}};var ut=0,ne=new Map;function ft(e,r){return e.replace(/\{([\w.]+)\}/g,(t,o)=>{let i=o.split(".").reduce((a,c)=>a==null?void 0:a[c],r);return i==null?"":String(i)})}var re=new Set;function pt(e,r){let t=r||{};re.forEach(o=>{if(!(o in t)){try{e.clearTargeting(o)}catch(i){}re.delete(o)}}),Object.entries(t).forEach(([o,i])=>{e.setTargeting(o,i),re.add(o)})}var _e={name:"gpt",async init(e,r){window.googletag=window.googletag||{cmd:[]};let t=window.googletag;t.cmd.push(()=>{let o=t.pubads();e.singleRequest!==!1&&o.enableSingleRequest(),o.disableInitialLoad(),o.collapseEmptyDivs(!1),e.nonPersonalizedWithoutConsent&&r.consent&&!r.consent.granted&&o.setPrivacySettings&&o.setPrivacySettings({nonPersonalizedAds:!0}),t.enableServices()}),await $(e.src||"https://securepubads.g.doubleclick.net/tag/js/gpt.js",{crossorigin:"anonymous"})},fill(e,r,t){return new Promise(o=>{let i=window.googletag,a={},c=e.length;i.cmd.push(()=>{let l=i.pubads();pt(l,t.targeting);let u=[],f={};if(e.forEach(p=>{let v=document.createElement("div");v.id="ual-gpt-"+ ++ut,p.el.appendChild(v);let M=p.params.adUnitPath||ft(r.adUnitPath,{network:r.network,site:t.site,slot:p.name,section:p.params.section,params:p.params}),z=p.sizes.length?p.sizes:["fluid"],k=i.defineSlot(M,z,v.id);if(!k){a[p.id]={filled:!1},c--;return}k.addService(l),Object.entries(p.params.targeting||{}).forEach(([_,U])=>k.setTargeting(_,U)),ne.set(p.id,k),f[v.id]=p.id,u.push(k),i.display(v.id)}),!c)return o(a);let y,E=()=>{clearTimeout(y),l.removeEventListener("slotRenderEnded",A),Object.values(f).forEach(p=>{a[p]||(a[p]={filled:!1})}),o(a)},A=p=>{let v=f[p.slot.getSlotElementId()];if(!v||a[v])return;let[M,z]=Array.isArray(p.size)?p.size:[0,0];a[v]={filled:!p.isEmpty,width:M,height:z,meta:{advertiserId:p.advertiserId,lineItemId:p.lineItemId,creativeId:p.creativeId}},--c===0&&E()};l.addEventListener("slotRenderEnded",A),y=setTimeout(E,r.fillTimeoutMs||4e3),l.refresh(u)})})},destroy(e){let r=ne.get(e.id);r&&window.googletag&&window.googletag.destroySlots&&(window.googletag.destroySlots([r]),ne.delete(e.id))}};var Ue={name:"adsense",async init(e){await $(`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${encodeURIComponent(e.client)}`,{crossorigin:"anonymous"})},fill(e,r){return Promise.all(e.map(t=>new Promise(o=>{let i=document.createElement("ins");i.className="adsbygoogle",i.setAttribute("data-ad-client",r.client),i.setAttribute("data-ad-slot",t.params.slot);let[a,c]=t.sizes[0]||[];t.params.format||!a?(i.style.display="block",i.setAttribute("data-ad-format",t.params.format||"auto"),t.params.responsive!==!1&&i.setAttribute("data-full-width-responsive","true")):i.style.cssText=`display:inline-block;width:${a}px;height:${c}px`,r.test&&i.setAttribute("data-adtest","on"),t.el.appendChild(i);let l=y=>{u.disconnect(),clearTimeout(f);let E=i.getBoundingClientRect();o([t.id,{filled:y,width:Math.round(E.width),height:Math.round(E.height)}])},u=new MutationObserver(()=>{let y=i.getAttribute("data-ad-status");y==="filled"?l(!0):y==="unfilled"&&l(!1)});u.observe(i,{attributes:!0,attributeFilter:["data-ad-status"]});let f=setTimeout(()=>l(!!i.querySelector("iframe")),r.fillTimeoutMs||4e3);try{(window.adsbygoogle=window.adsbygoogle||[]).push({})}catch(y){l(!1)}}))).then(t=>Object.fromEntries(t))}};function mt(e){let r=e.reduce((o,i)=>o+(i.weight||1),0),t=Math.random()*r;for(let o of e)if(t-=o.weight||1,t<=0)return o;return e[e.length-1]}var Ne={name:"house",async request(e,r,t){let o=Date.now(),i={};return e.forEach(a=>{let c=(r.creatives||[]).filter(l=>!(l.slots&&!l.slots.includes(a.name)||l.devices&&!l.devices.includes(t.device)||l.from&&o<Date.parse(l.from)||l.to&&o>Date.parse(l.to)+864e5||a.sizes.length&&!a.sizes.some(([u,f])=>u===l.width&&f===l.height)));if(c.length){let l=mt(c);i[a.id]={...l,type:l.type||(l.html?"html":"image")}}}),i}};var gt="0.1.1";function De(){let e=new Map(Object.entries({json:Pe,iframe:Le,revive:qe,"revive-async":Re,gpt:_e,adsense:Ue,house:Ne}));return{...$e(e),version:gt}}var Fe="ual";(function(){let r=window[Fe]||{};if(r.version)return;let o={...De(),cmd:{push:u=>i(u)}};function i(u){try{u(o)}catch(f){S("Errore in comando cmd",f)}}let a=Array.isArray(r.cmd)?r.cmd:[];window[Fe]=o;let c=document.currentScript||document.querySelector('script[data-ual-config],script[data-config][src*="ual"]'),l=c&&(c.getAttribute("data-ual-config")||c.getAttribute("data-config"));if(a.forEach(i),l){let u=()=>o.init(l).catch(f=>S("Init fallito",f));document.readyState==="loading"?document.addEventListener("DOMContentLoaded",u):u()}})();})();
package/dist/vue.cjs ADDED
@@ -0,0 +1,128 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/vue.js
20
+ var vue_exports = {};
21
+ __export(vue_exports, {
22
+ AdSlot: () => AdSlot,
23
+ UalProvider: () => UalProvider,
24
+ useUal: () => useUal
25
+ });
26
+ module.exports = __toCommonJS(vue_exports);
27
+ var import_vue = require("vue");
28
+
29
+ // src/wrappers/start.js
30
+ var import_lib = require("./index.cjs");
31
+ var loading = /* @__PURE__ */ new Map();
32
+ function loadFromCdn(src) {
33
+ if (window.ual && window.ual.version) return Promise.resolve(window.ual);
34
+ if (!loading.has(src)) {
35
+ loading.set(src, new Promise((resolve, reject) => {
36
+ const s = document.createElement("script");
37
+ s.async = true;
38
+ s.src = src;
39
+ s.onload = () => {
40
+ if (window.ual && window.ual.version) resolve(window.ual);
41
+ else reject(new Error("il loader caricato da " + src + " non ha esposto window.ual"));
42
+ };
43
+ s.onerror = () => {
44
+ loading.delete(src);
45
+ reject(new Error("impossibile caricare il loader da " + src));
46
+ };
47
+ document.head.appendChild(s);
48
+ }));
49
+ }
50
+ return loading.get(src);
51
+ }
52
+ async function startUal({ config, src, beforeInit }) {
53
+ const ual = src ? await loadFromCdn(src) : (0, import_lib.getUal)();
54
+ if (beforeInit) beforeInit(ual);
55
+ const started = typeof ual.isInitialized === "function" && ual.isInitialized();
56
+ if (started) {
57
+ const p = typeof ual.whenReady === "function" ? ual.whenReady() : null;
58
+ if (p) await p;
59
+ } else {
60
+ await ual.init(config);
61
+ }
62
+ return ual;
63
+ }
64
+
65
+ // src/vue.js
66
+ var KEY = /* @__PURE__ */ Symbol("ual");
67
+ function useUal() {
68
+ return (0, import_vue.inject)(KEY, { ual: (0, import_vue.shallowRef)(null), ready: (0, import_vue.ref)(false) });
69
+ }
70
+ var UalProvider = (0, import_vue.defineComponent)({
71
+ name: "UalProvider",
72
+ props: {
73
+ config: { type: [Object, String] },
74
+ src: { type: String },
75
+ pageKey: { type: [String, Number] },
76
+ targeting: { type: Object }
77
+ },
78
+ emits: ["event"],
79
+ setup(props, { slots, emit }) {
80
+ const ual = (0, import_vue.shallowRef)(null);
81
+ const ready = (0, import_vue.ref)(false);
82
+ (0, import_vue.provide)(KEY, { ual, ready });
83
+ let off = null;
84
+ let mounted = false;
85
+ (0, import_vue.onMounted)(() => {
86
+ mounted = true;
87
+ startUal({
88
+ config: props.config,
89
+ src: props.src,
90
+ // eventi e targeting si agganciano PRIMA dell'init, così non si perde nulla
91
+ beforeInit: (u) => {
92
+ if (!mounted) return;
93
+ if (props.targeting) u.setTargeting(props.targeting);
94
+ off = u.on("*", (data, evt) => emit("event", evt, data));
95
+ }
96
+ }).then((u) => {
97
+ ual.value = u;
98
+ ready.value = true;
99
+ }).catch((e) => console.warn("[UAL]", e && e.message ? e.message : e));
100
+ });
101
+ (0, import_vue.onBeforeUnmount)(() => {
102
+ mounted = false;
103
+ if (off) off();
104
+ });
105
+ (0, import_vue.watch)(() => props.pageKey, () => {
106
+ if (ual.value) ual.value.pageView();
107
+ });
108
+ (0, import_vue.watch)(() => props.targeting, (t) => {
109
+ if (ual.value && t) ual.value.setTargeting(t);
110
+ }, { deep: true });
111
+ return () => slots.default ? slots.default() : null;
112
+ }
113
+ });
114
+ var AdSlot = (0, import_vue.defineComponent)({
115
+ name: "AdSlot",
116
+ props: {
117
+ name: { type: String, required: true },
118
+ params: { type: Object },
119
+ minHeight: { type: Number }
120
+ },
121
+ setup(props) {
122
+ return () => (0, import_vue.h)("div", {
123
+ "data-ad-slot": props.name,
124
+ "data-ad-params": props.params ? JSON.stringify(props.params) : void 0,
125
+ style: props.minHeight ? { minHeight: props.minHeight + "px" } : void 0
126
+ });
127
+ }
128
+ });
package/dist/vue.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { DefineComponent, Ref, ShallowRef } from 'vue';
2
+ import type { Ual, UalConfig, Targeting, UalEventMap } from './index';
3
+
4
+ /** Avvia UAL dopo il montaggio (solo browser), una volta per pagina. Emette `event` per gli eventi di UAL. */
5
+ export const UalProvider: DefineComponent<{
6
+ config: UalConfig | string;
7
+ src?: string;
8
+ pageKey?: string | number;
9
+ targeting?: Targeting;
10
+ }, {}, {}, {}, {}, {}, {}, {
11
+ event: <K extends keyof UalEventMap>(event: K, data: UalEventMap[K]) => void;
12
+ }>;
13
+
14
+ /** Una posizione pubblicitaria: un div vuoto che UAL riempie. */
15
+ export const AdSlot: DefineComponent<{
16
+ name: string;
17
+ params?: Record<string, unknown>;
18
+ minHeight?: number;
19
+ }>;
20
+
21
+ export function useUal(): { ual: ShallowRef<Ual | null>; ready: Ref<boolean> };
package/dist/vue.mjs ADDED
@@ -0,0 +1,108 @@
1
+ // src/vue.js
2
+ import { defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, shallowRef, watch } from "vue";
3
+
4
+ // src/wrappers/start.js
5
+ import { getUal } from "./index.mjs";
6
+ var loading = /* @__PURE__ */ new Map();
7
+ function loadFromCdn(src) {
8
+ if (window.ual && window.ual.version) return Promise.resolve(window.ual);
9
+ if (!loading.has(src)) {
10
+ loading.set(src, new Promise((resolve, reject) => {
11
+ const s = document.createElement("script");
12
+ s.async = true;
13
+ s.src = src;
14
+ s.onload = () => {
15
+ if (window.ual && window.ual.version) resolve(window.ual);
16
+ else reject(new Error("il loader caricato da " + src + " non ha esposto window.ual"));
17
+ };
18
+ s.onerror = () => {
19
+ loading.delete(src);
20
+ reject(new Error("impossibile caricare il loader da " + src));
21
+ };
22
+ document.head.appendChild(s);
23
+ }));
24
+ }
25
+ return loading.get(src);
26
+ }
27
+ async function startUal({ config, src, beforeInit }) {
28
+ const ual = src ? await loadFromCdn(src) : getUal();
29
+ if (beforeInit) beforeInit(ual);
30
+ const started = typeof ual.isInitialized === "function" && ual.isInitialized();
31
+ if (started) {
32
+ const p = typeof ual.whenReady === "function" ? ual.whenReady() : null;
33
+ if (p) await p;
34
+ } else {
35
+ await ual.init(config);
36
+ }
37
+ return ual;
38
+ }
39
+
40
+ // src/vue.js
41
+ var KEY = /* @__PURE__ */ Symbol("ual");
42
+ function useUal() {
43
+ return inject(KEY, { ual: shallowRef(null), ready: ref(false) });
44
+ }
45
+ var UalProvider = defineComponent({
46
+ name: "UalProvider",
47
+ props: {
48
+ config: { type: [Object, String] },
49
+ src: { type: String },
50
+ pageKey: { type: [String, Number] },
51
+ targeting: { type: Object }
52
+ },
53
+ emits: ["event"],
54
+ setup(props, { slots, emit }) {
55
+ const ual = shallowRef(null);
56
+ const ready = ref(false);
57
+ provide(KEY, { ual, ready });
58
+ let off = null;
59
+ let mounted = false;
60
+ onMounted(() => {
61
+ mounted = true;
62
+ startUal({
63
+ config: props.config,
64
+ src: props.src,
65
+ // eventi e targeting si agganciano PRIMA dell'init, così non si perde nulla
66
+ beforeInit: (u) => {
67
+ if (!mounted) return;
68
+ if (props.targeting) u.setTargeting(props.targeting);
69
+ off = u.on("*", (data, evt) => emit("event", evt, data));
70
+ }
71
+ }).then((u) => {
72
+ ual.value = u;
73
+ ready.value = true;
74
+ }).catch((e) => console.warn("[UAL]", e && e.message ? e.message : e));
75
+ });
76
+ onBeforeUnmount(() => {
77
+ mounted = false;
78
+ if (off) off();
79
+ });
80
+ watch(() => props.pageKey, () => {
81
+ if (ual.value) ual.value.pageView();
82
+ });
83
+ watch(() => props.targeting, (t) => {
84
+ if (ual.value && t) ual.value.setTargeting(t);
85
+ }, { deep: true });
86
+ return () => slots.default ? slots.default() : null;
87
+ }
88
+ });
89
+ var AdSlot = defineComponent({
90
+ name: "AdSlot",
91
+ props: {
92
+ name: { type: String, required: true },
93
+ params: { type: Object },
94
+ minHeight: { type: Number }
95
+ },
96
+ setup(props) {
97
+ return () => h("div", {
98
+ "data-ad-slot": props.name,
99
+ "data-ad-params": props.params ? JSON.stringify(props.params) : void 0,
100
+ style: props.minHeight ? { minHeight: props.minHeight + "px" } : void 0
101
+ });
102
+ }
103
+ });
104
+ export {
105
+ AdSlot,
106
+ UalProvider,
107
+ useUal
108
+ };
@@ -0,0 +1,78 @@
1
+ # Connecting existing ad servers and banner exchanges
2
+
3
+ The systems already in use (AdManager, Banner Exchange Pro, Banner Exchange and the two legacy PHP ones) connect to UAL **without modifying them**, using the `iframe` adapter: one provider per system, with the ID in the URL coming from the slot's `params`.
4
+
5
+ > There is also an HTML version with examples that open in the configurator: `ual-configurator`, `/docs/` page ("I tuoi ad server" section).
6
+
7
+ ## The systems
8
+
9
+ | Project | What it does | Public endpoints | When it is "empty" |
10
+ |---|---|---|---|
11
+ | `admanager-next` | Ad server: units with image or HTML-code ads, priorities, dates, impression and click caps, statistics, Stripe shop | `GET /api/iframe/{unit}`, `GET /api/embed/{unit}` (JS), `GET /api/click/{ad}` | No eligible ad: iframe with empty body, empty JS |
12
+ | `bannerpro-next` | Credit-based banner exchange, 468×60 | `GET /api/display/{id}` (`?js=1`), `GET /api/click/{id}?hostid=` | Never: with no candidate it shows the "house" banner |
13
+ | `banner-next` | Simpler banner exchange | `GET /api/work/{username}` | Empty body if there is no banner |
14
+ | `banner/` (legacy PHP) | Original of `bannerpro-next` | `display.php?id=N` (`&js=1`), `click.php?hostid=&targetid=`, `banner.php` (house) | Never: with no candidate it shows a nested iframe with `banner.php` |
15
+ | `banner/admanager/` (legacy PHP) | Original of `admanager-next`, without priorities, dates, caps or placeholder | `admanager/iframe.php?id=U`, `admanager/embed.php?id=U`, `admanager/click.php?ad=` | No ad: empty body |
16
+
17
+ The Next rewrites and the legacy PHP share the same logic (rotation, credits, click counting).
18
+
19
+ ## Configuration
20
+
21
+ Replace the domains with your own. The slot sizes must match the banner's: 468×60 for the exchanges (default value, editable in each system's settings), the unit's size for the ad servers.
22
+
23
+ | System | URL | `params` | Sandbox |
24
+ |---|---|---|---|
25
+ | AdManager (Next) | `https://…/api/iframe/{params.unit}` | `{"unit": unit id}` | default |
26
+ | AdManager (legacy) | `https://…/banner/admanager/iframe.php?id={params.unit}` | `{"unit": unit id}` | default |
27
+ | Banner Exchange Pro (Next) | `https://…/api/display/{params.host}` | `{"host": member id}` | with top navigation |
28
+ | Banner Exchange (Next) | `https://…/api/work/{params.user}` | `{"user": "username"}` (letters and numbers, max 15) | with top navigation |
29
+ | Banner exchange (legacy) | `https://…/banner/display.php?id={params.host}` | `{"host": member id}` | default |
30
+
31
+ ```json
32
+ {
33
+ "site": "my-site",
34
+ "consent": { "mode": "tcf" },
35
+ "providers": {
36
+ "adm_next": { "adapter": "iframe", "consent": "contextual", "url": "https://admanager.yourdomain.net/api/iframe/{params.unit}" },
37
+ "adm_legacy": { "adapter": "iframe", "consent": "contextual", "url": "https://banner.yourdomain.net/admanager/iframe.php?id={params.unit}" },
38
+ "exch_pro": { "adapter": "iframe", "consent": "contextual", "url": "https://exchange.yourdomain.net/api/display/{params.host}",
39
+ "sandbox": "allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-top-navigation-by-user-activation" },
40
+ "exch_next": { "adapter": "iframe", "consent": "contextual", "url": "https://exchange2.yourdomain.net/api/work/{params.user}",
41
+ "sandbox": "allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-top-navigation-by-user-activation" },
42
+ "exch_legacy": { "adapter": "iframe", "consent": "contextual", "url": "https://banner.yourdomain.net/display.php?id={params.host}" }
43
+ },
44
+ "slots": {
45
+ "top": { "sizes": [[728, 90]], "minHeight": 90, "providers": [{ "id": "adm_next", "params": { "unit": 3 } }] },
46
+ "box": { "sizes": [[300, 250]], "providers": [{ "id": "adm_legacy", "params": { "unit": 5 } }] },
47
+ "footer1": { "sizes": [[468, 60]], "providers": [{ "id": "exch_pro", "params": { "host": 7 } }] },
48
+ "footer2": { "sizes": [[468, 60]], "providers": [{ "id": "exch_next", "params": { "user": "mario" } }] },
49
+ "footer3": { "sizes": [[468, 60]], "providers": [{ "id": "exch_legacy", "params": { "host": 7 } }] }
50
+ }
51
+ }
52
+ ```
53
+
54
+ This is the configurator's "Your existing ad servers" example: it opens from `?esempio=ad-server-esistenti`.
55
+
56
+ ## Limits
57
+
58
+ - **One system per slot.** The `iframe` adapter cannot know whether the banner is empty (the iframe has a different origin), so it always considers the slot filled and the waterfall stops there. Put it alone in the slot or at the last level. The exchanges show a default banner anyway when they have no candidate; the ad servers leave a blank box if the unit is empty.
59
+ - **`target="_top"` links.** `bannerpro-next` and `banner-next` open the banner with `target="_top"`. The default sandbox does not allow navigating the top page, so without the override shown above the click does nothing. AdManager and the legacy PHP use `_blank` and work with the default sandbox. This behavior is verified by the tests (`test/regression.mjs`, group "iframe stile AdManager/scambio").
60
+ - **IDs belong to each system.** `unit: 3` of AdManager (Next) and `unit: 3` of the legacy one are two different units in two different databases.
61
+ - **Separate statistics.** Impressions and clicks stay counted by the system serving the banner (impression on the iframe request, click on the click endpoint). UAL's "viewable" tracker does not reach these systems.
62
+ - **No selection by size.** AdManager's `pickAd` chooses by unit, not by requested size: each slot must point to a unit of the right size (different slots or units for mobile and desktop).
63
+ - **`contextual` consent.** The Next systems' routes that were read (embed, iframe, click, display, work) set no cookies. The legacy PHP calls `session_start()` in `click.php` (session cookie on click) and `config.php` has not been checked: check the `Set-Cookie` header of `display.php` and `iframe.php` before keeping `contextual`, and have the privacy setup reviewed by whoever is responsible for it.
64
+ - **AdManager `code` ads.** Today `/api/embed` inserts them into the page DOM when they contain `<script>`. In UAL they run in the isolated sandbox (separate iframe).
65
+
66
+ ## How to get a fallback after an ad server
67
+
68
+ To let the waterfall continue after an ad server (for example AdSense or the house promo), the server must answer "empty" in a way the browser can read. The contract is that of the `json` adapter (see the README): a `POST` with the ready slots, answered with `{ "ads": [...] }` or `204`.
69
+
70
+ For AdManager (Next) this would need an endpoint like `POST /api/ual/decide` that:
71
+
72
+ - reuses `pickAd` and filters by the requested sizes;
73
+ - answers `204` if there is nothing;
74
+ - returns a `clickUrl` pointing to `/api/click/{id}` (and `type: "image"` or `"html"` for `code` ads);
75
+ - handles CORS for the clients' domains;
76
+ - counts impressions with a signed tracker (HMAC) instead of at selection time, so an ad discarded by UAL is not counted.
77
+
78
+ It has not been implemented: the existing projects have not been modified.
package/docs/cdn.md ADDED
@@ -0,0 +1,58 @@
1
+ # Publishing UAL: the files to put on the CDN
2
+
3
+ With the `<script>` **tag**, or with the components in "On your CDN" mode (`src` + `config` as a URL), **two files** go on the CDN. An HTML version with examples is in the configurator guide (`/docs/#pubblicare`).
4
+
5
+ ## The files
6
+
7
+ | File | Where it comes from | Example address |
8
+ |---|---|---|
9
+ | **The loader** | `dist/ual.min.js` of this project, after `npm run build` (about 25 KB, 10 KB gzipped) | `https://adcdn.yourdomain.net/ual.min.js` |
10
+ | **The configuration** | the JSON downloaded from the configurator (*JSON* tab ▸ *Download*), named `<site-id>.json` | `https://adcdn.yourdomain.net/config/<site-id>.json` |
11
+
12
+ One configuration file per site. In the configurator's *Snippet* tab you set the domain, the loader name and the configuration path: the address it shows is the exact one and the generated snippet already points there.
13
+
14
+ ```
15
+ cdn/
16
+ ├── ual.min.js ← the loader: the same file for all sites
17
+ └── config/
18
+ ├── mario-pizzeria.json ← the configuration: one per site
19
+ └── another-site.json
20
+ ```
21
+
22
+ ## Headers
23
+
24
+ | File | Headers | Why |
25
+ |---|---|---|
26
+ | Configuration | `Content-Type: application/json`, `Access-Control-Allow-Origin: *`, `Cache-Control: public, max-age=120` | The loader reads it with `fetch` from another domain (without cookies, so `*` is fine). Short cache (1–5 minutes) so changes go live quickly |
27
+ | Loader | `Content-Type: text/javascript`, longer cache; enable gzip or brotli | Loaded with `<script src>`: no CORS |
28
+
29
+ For the loader it is best to use a versioned path plus an alias for urgent releases:
30
+
31
+ ```
32
+ cdn/
33
+ ├── v0.1.0/ual.min.js ← long cache (public, max-age=31536000, immutable)
34
+ ├── ual.min.js ← alias, short cache (public, max-age=300)
35
+ └── config/
36
+ └── mario-pizzeria.json
37
+ ```
38
+
39
+ ## What is not needed
40
+
41
+ - `index.mjs`, `react.mjs`, `vue.mjs` and the other files in `dist/` other than `ual.min.js`: they are for the npm package.
42
+ - `dist/ual.js` (not minified): optional, only useful to read the code in the browser while debugging.
43
+ - The JSON schema, the documentation and the types.
44
+
45
+ ## Special cases
46
+
47
+ - **Package with the config in the code** (`defineConfig`, component without `src`): nothing is needed on the CDN, UAL and the configuration are in the site bundle.
48
+ - **Existing ad servers only** (`iframe` adapter, see `ad-server-esistenti.md`): the same two files apply; the IDs and URLs are already in the configuration.
49
+ - **House creative images** (e.g. `https://cdn.yourdomain.net/promo/728x90.webp`): they are a third type of file, uploaded wherever you like and referenced in the configuration. They are not part of UAL and need no CORS headers (they are shown in an isolated iframe).
50
+
51
+ ## Publishing and updating
52
+
53
+ 1. Download the JSON from the configurator.
54
+ 2. Upload it to the CDN at the address shown in *Snippet*; also upload `ual.min.js` (once, or with every new loader version).
55
+ 3. Paste the snippet (or the framework code) into the site.
56
+ 4. **To change providers, slots or rules:** edit in the configurator, download the JSON again and upload it **to the same address**. Sites pick it up on the next visit, after the cache expires.
57
+
58
+ In SPAs using the tag, `ual.pageView()` must be called on every page change (the components do it on their own). Before replacing the configuration of a live site, publish with a test ID and check the log with `?ual_debug=1`.
@@ -0,0 +1,156 @@
1
+ # Complete example: client site with its own ad server, AdSense and house promos
2
+
3
+ ## 1. On the client's site
4
+
5
+ ```html
6
+ <head>
7
+ <script async src="https://adcdn.yourdomain.net/ual.min.js"
8
+ data-config="https://adcdn.yourdomain.net/config/mario-pizzeria.json"></script>
9
+ </head>
10
+ <body>
11
+ <div data-ad-slot="top"></div>
12
+ <article>…</article>
13
+ <div data-ad-slot="sidebar"></div>
14
+ <div data-ad-slot="bottom"></div>
15
+ </body>
16
+ ```
17
+
18
+ ## 2. Configuration (`mario-pizzeria.json` on the CDN)
19
+
20
+ ```json
21
+ {
22
+ "site": "mario-pizzeria",
23
+ "consent": { "mode": "tcf", "requiredPurposes": [1] },
24
+ "targeting": { "category": "food", "city": "milan" },
25
+
26
+ "providers": {
27
+ "mine": { "adapter": "json", "endpoint": "https://ads.yourdomain.net/v1/decide" },
28
+ "adsense": { "adapter": "adsense", "client": "ca-pub-1111111111", "consent": "required" },
29
+ "promo": { "adapter": "house", "creatives": [
30
+ { "type": "image", "src": "https://adcdn.yourdomain.net/promo/728x90.webp",
31
+ "width": 728, "height": 90, "clickUrl": "https://horbit.it" },
32
+ { "type": "image", "src": "https://adcdn.yourdomain.net/promo/320x100.webp",
33
+ "width": 320, "height": 100, "clickUrl": "https://horbit.it" }
34
+ ]}
35
+ },
36
+
37
+ "slots": {
38
+ "top": {
39
+ "label": "Advertising", "minHeight": 90, "lazy": false,
40
+ "sizeMapping": [
41
+ { "minWidth": 0, "sizes": [[320, 100]] },
42
+ { "minWidth": 768, "sizes": [[728, 90]] }
43
+ ],
44
+ "providers": [{ "id": "mine", "params": { "zone": 10 } }, "promo"]
45
+ },
46
+ "sidebar": {
47
+ "sizes": [[300, 250]],
48
+ "providers": [{ "id": "mine", "params": { "zone": 11 } },
49
+ { "id": "adsense", "params": { "slot": "2222222222" } }]
50
+ },
51
+ "bottom": {
52
+ "sizes": [[728, 90]],
53
+ "providers": [{ "id": "mine", "params": { "zone": 12 } }]
54
+ }
55
+ },
56
+
57
+ "pageRules": [
58
+ { "match": "^/menu", "disable": ["bottom"], "targeting": { "section": "menu" } }
59
+ ]
60
+ }
61
+ ```
62
+
63
+ ## 3. Flow on a desktop visit to the home page
64
+
65
+ 1. The loader reads the config and waits for the CMP.
66
+ 2. `top` and `sidebar` are in the viewport and start together with **a single** call:
67
+
68
+ ```json
69
+ POST https://ads.yourdomain.net/v1/decide
70
+ {
71
+ "site": "mario-pizzeria",
72
+ "page": { "url": "https://mariopizzeria.it/", "path": "/" },
73
+ "device": "desktop",
74
+ "targeting": { "category": "food", "city": "milan" },
75
+ "consent": { "gdprApplies": true, "tcString": "CQ…", "granted": true },
76
+ "slots": [
77
+ { "id": "top#1", "name": "top", "sizes": [[728,90]], "params": { "zone": 10 } },
78
+ { "id": "sidebar#2", "name": "sidebar", "sizes": [[300,250]], "params": { "zone": 11 } }
79
+ ]
80
+ }
81
+ ```
82
+
83
+ 3. The server has a campaign only for `top`:
84
+
85
+ ```json
86
+ { "ads": [ {
87
+ "slotId": "top#1", "type": "image",
88
+ "src": "https://adcdn.yourdomain.net/c/beer-xyz-728.webp",
89
+ "width": 728, "height": 90,
90
+ "clickUrl": "https://ads.yourdomain.net/c?t=eyJ…",
91
+ "trackers": {
92
+ "impression": ["https://ads.yourdomain.net/i?t=eyJ…"],
93
+ "viewable": ["https://ads.yourdomain.net/v?t=eyJ…"]
94
+ }
95
+ } ] }
96
+ ```
97
+
98
+ 4. Result:
99
+ - **top**: banner shown in an isolated iframe, impression tracker fired; after 1 s at 50% visibility the viewable tracker fires.
100
+ - **sidebar**: empty on your server → the client's AdSense (loaded only because consent is present).
101
+ - **bottom**: requested only when the user scrolls near it; if empty, the slot hides.
102
+ - **/menu**: `bottom` is not loaded, the targeting includes `section: menu`.
103
+
104
+ ## 4. Minimal `decide` endpoint (Node/Fastify)
105
+
106
+ ```js
107
+ import Fastify from 'fastify';
108
+ const app = Fastify();
109
+
110
+ // CORS: a POST with a JSON Content-Type from another domain first triggers an OPTIONS preflight.
111
+ // Without these headers and without the OPTIONS route the browser blocks every request.
112
+ // ('*' is fine because the adapter sends no cookies; with withCredentials you need the exact origin.)
113
+ app.addHook('onRequest', async (req, reply) => {
114
+ reply.header('Access-Control-Allow-Origin', '*');
115
+ reply.header('Access-Control-Allow-Methods', 'POST, OPTIONS');
116
+ reply.header('Access-Control-Allow-Headers', 'Content-Type');
117
+ reply.header('Access-Control-Max-Age', '600');
118
+ });
119
+ app.options('/v1/decide', async (req, reply) => reply.code(204).send());
120
+
121
+ app.post('/v1/decide', async (req, reply) => {
122
+ if (!req.body || !Array.isArray(req.body.slots)) return reply.code(400).send();
123
+ const { site, slots, consent, targeting } = req.body;
124
+
125
+ const ads = [];
126
+ for (const s of slots) {
127
+ // placeholder: banner selection in your ad server
128
+ const banner = await myAdServer.pick({ site, zone: s.params.zone, sizes: s.sizes, targeting });
129
+ if (!banner) continue; // empty -> next provider
130
+
131
+ const t = sign({ b: banner.id, s: site, z: s.params.zone }); // placeholder: HMAC token
132
+ ads.push({
133
+ slotId: s.id, type: 'image', src: banner.imageUrl,
134
+ width: banner.w, height: banner.h,
135
+ clickUrl: `https://ads.yourdomain.net/c?t=${t}`,
136
+ trackers: {
137
+ impression: [`https://ads.yourdomain.net/i?t=${t}`],
138
+ viewable: [`https://ads.yourdomain.net/v?t=${t}`],
139
+ },
140
+ });
141
+ }
142
+ return ads.length ? { ads } : reply.code(204).send();
143
+ });
144
+ ```
145
+
146
+ `/c`, `/i`, `/v` verify the token and record the event; `/c` redirects to the landing page.
147
+
148
+ ## 5. Page-side events
149
+
150
+ ```js
151
+ window.ual = window.ual || { cmd: [] };
152
+ ual.cmd.push(u => {
153
+ u.on('slotRendered', e => console.log('shown', e.slot, 'by', e.provider));
154
+ u.on('slotEmpty', e => console.log('empty', e.slot));
155
+ });
156
+ ```