universal-ad-loader 0.0.0-stage → 0.1.0
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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +208 -2
- package/astro/AdSlot.astro +23 -0
- package/astro/UalBundled.astro +34 -0
- package/astro/UalLoader.astro +40 -0
- package/dist/global.d.ts +13 -0
- package/dist/index.cjs +1288 -0
- package/dist/index.d.ts +281 -0
- package/dist/index.mjs +1266 -0
- package/dist/next.cjs +35 -0
- package/dist/next.d.ts +8 -0
- package/dist/next.mjs +16 -0
- package/dist/react.cjs +122 -0
- package/dist/react.d.ts +36 -0
- package/dist/react.mjs +103 -0
- package/dist/ual.js +1277 -0
- package/dist/ual.min.js +12 -0
- package/dist/vue.cjs +128 -0
- package/dist/vue.d.ts +21 -0
- package/dist/vue.mjs +108 -0
- package/docs/ad-server-esistenti.md +78 -0
- package/docs/cdn.md +58 -0
- package/docs/esempio.md +156 -0
- package/docs/libreria.md +142 -0
- package/docs/pubblicazione-npm.md +114 -0
- package/package.json +119 -4
- package/schema/ual-config.schema.json +191 -0
package/dist/ual.min.js
ADDED
|
@@ -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,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">")}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.0";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
|
+
# Collegare gli ad server e gli scambi banner esistenti
|
|
2
|
+
|
|
3
|
+
I sistemi già in uso (AdManager, Banner Exchange Pro, Banner Exchange e i due PHP legacy) si collegano a UAL **senza modificarli**, con l'adapter `iframe`: un provider per sistema, con l'id nell'URL che arriva dai `params` dello slot.
|
|
4
|
+
|
|
5
|
+
> Esiste anche una versione HTML con esempi apribili nel configuratore: `ual-configurator`, pagina `/docs/` (sezione "I tuoi ad server").
|
|
6
|
+
|
|
7
|
+
## I sistemi
|
|
8
|
+
|
|
9
|
+
| Progetto | Cosa fa | Endpoint pubblici | Quando è "vuoto" |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `admanager-next` | Ad server: unità con annunci immagine o codice HTML, priorità, date, tetti di impression e click, statistiche, shop con Stripe | `GET /api/iframe/{unità}`, `GET /api/embed/{unità}` (JS), `GET /api/click/{annuncio}` | Nessun annuncio idoneo: iframe con body vuoto, JS vuoto |
|
|
12
|
+
| `bannerpro-next` | Scambio banner a crediti, 468×60 | `GET /api/display/{id}` (`?js=1`), `GET /api/click/{id}?hostid=` | Mai: senza candidato mostra il banner "house" |
|
|
13
|
+
| `banner-next` | Scambio banner più semplice | `GET /api/work/{username}` | Body vuoto se non c'è un banner |
|
|
14
|
+
| `banner/` (PHP legacy) | Originale di `bannerpro-next` | `display.php?id=N` (`&js=1`), `click.php?hostid=&targetid=`, `banner.php` (house) | Mai: senza candidato mostra un iframe annidato con `banner.php` |
|
|
15
|
+
| `banner/admanager/` (PHP legacy) | Originale di `admanager-next`, senza priorità, date, tetti né segnaposto | `admanager/iframe.php?id=U`, `admanager/embed.php?id=U`, `admanager/click.php?ad=` | Nessun annuncio: body vuoto |
|
|
16
|
+
|
|
17
|
+
Le riscritture Next e il PHP legacy hanno la stessa logica (rotazione, crediti, conteggio click).
|
|
18
|
+
|
|
19
|
+
## Configurazione
|
|
20
|
+
|
|
21
|
+
Sostituisci i domini con i tuoi. Le dimensioni dello slot devono coincidere con quelle del banner: 468×60 per gli scambi (valore di default, modificabile dalle impostazioni di ciascun sistema), la dimensione dell'unità per gli ad server.
|
|
22
|
+
|
|
23
|
+
| Sistema | URL | `params` | Sandbox |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| AdManager (Next) | `https://…/api/iframe/{params.unit}` | `{"unit": id dell'unità}` | default |
|
|
26
|
+
| AdManager (legacy) | `https://…/banner/admanager/iframe.php?id={params.unit}` | `{"unit": id dell'unità}` | default |
|
|
27
|
+
| Banner Exchange Pro (Next) | `https://…/api/display/{params.host}` | `{"host": id del membro}` | con navigazione top |
|
|
28
|
+
| Banner Exchange (Next) | `https://…/api/work/{params.user}` | `{"user": "username"}` (lettere e numeri, max 15) | con navigazione top |
|
|
29
|
+
| Scambio banner (legacy) | `https://…/banner/display.php?id={params.host}` | `{"host": id del membro}` | default |
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"site": "mio-sito",
|
|
34
|
+
"consent": { "mode": "tcf" },
|
|
35
|
+
"providers": {
|
|
36
|
+
"adm_next": { "adapter": "iframe", "consent": "contextual", "url": "https://admanager.tuodominio.net/api/iframe/{params.unit}" },
|
|
37
|
+
"adm_legacy": { "adapter": "iframe", "consent": "contextual", "url": "https://banner.tuodominio.net/admanager/iframe.php?id={params.unit}" },
|
|
38
|
+
"exch_pro": { "adapter": "iframe", "consent": "contextual", "url": "https://scambio.tuodominio.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://scambio2.tuodominio.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.tuodominio.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
|
+
È l'esempio "I tuoi ad server esistenti" del configuratore: si apre da `?esempio=ad-server-esistenti`.
|
|
55
|
+
|
|
56
|
+
## Limiti
|
|
57
|
+
|
|
58
|
+
- **Un solo sistema per slot.** L'adapter `iframe` non può sapere se il banner è vuoto (l'iframe è di un'altra origine), quindi considera sempre pieno lo slot e il waterfall si ferma lì. Mettilo da solo nello slot o all'ultimo livello. Gli scambi mostrano comunque un banner di default quando non hanno un candidato; gli ad server lasciano un riquadro bianco se l'unità è vuota.
|
|
59
|
+
- **Link `target="_top"`.** `bannerpro-next` e `banner-next` aprono il banner con `target="_top"`. La sandbox di default non permette di navigare la pagina top, quindi senza l'override mostrato sopra il click non fa nulla. AdManager e i PHP legacy usano `_blank` e funzionano con la sandbox di default. Questo comportamento è verificato dai test (`test/regression.mjs`, gruppo "iframe stile AdManager/scambio").
|
|
60
|
+
- **Gli id sono di ciascun sistema.** `unit: 3` di AdManager (Next) e `unit: 3` di quello legacy sono due unità diverse in due database diversi.
|
|
61
|
+
- **Statistiche separate.** Impression e click restano contati dal sistema che serve il banner (impression alla richiesta dell'iframe, click all'endpoint di click). Il tracker "viewable" di UAL non arriva a questi sistemi.
|
|
62
|
+
- **Nessuna selezione per dimensione.** `pickAd` di AdManager sceglie per unità, non per dimensione richiesta: ogni slot deve puntare a un'unità della misura giusta (slot o unità diversi per mobile e desktop).
|
|
63
|
+
- **Consenso `contextual`.** Le route dei sistemi Next lette (embed, iframe, click, display, work) non impostano cookie. Il PHP legacy chiama `session_start()` in `click.php` (cookie di sessione al click) e `config.php` non è stato verificato: controlla l'header `Set-Cookie` di `display.php` e `iframe.php` prima di tenere `contextual`, e fai valutare l'impostazione privacy a chi se ne occupa.
|
|
64
|
+
- **Annunci `code` di AdManager.** Oggi `/api/embed` li inserisce nel DOM della pagina quando contengono `<script>`. In UAL girano nella sandbox isolata (iframe separato).
|
|
65
|
+
|
|
66
|
+
## Come avere il fallback dopo un ad server
|
|
67
|
+
|
|
68
|
+
Per far proseguire il waterfall dopo un ad server (per esempio AdSense o l'autopromo) il server deve rispondere "vuoto" in modo leggibile dal browser. Il contratto è quello dell'adapter `json` (vedi README): una `POST` con gli slot pronti, risposta `{ "ads": [...] }` oppure `204`.
|
|
69
|
+
|
|
70
|
+
Per AdManager (Next) servirebbe un endpoint tipo `POST /api/ual/decide` che:
|
|
71
|
+
|
|
72
|
+
- riusa `pickAd` e filtra per le dimensioni richieste;
|
|
73
|
+
- risponde `204` se non c'è nulla;
|
|
74
|
+
- restituisce `clickUrl` verso `/api/click/{id}` (e `type: "image"` oppure `"html"` per gli annunci `code`);
|
|
75
|
+
- gestisce il CORS per i domini dei clienti;
|
|
76
|
+
- conta le impression con un tracker firmato (HMAC, vedi "Prossimi passi" in `CLAUDE.md`) invece che alla scelta, così un annuncio scartato da UAL non viene contato.
|
|
77
|
+
|
|
78
|
+
Non è stato implementato: i progetti esistenti non sono stati modificati.
|
package/docs/cdn.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Pubblicare UAL: i file da mettere sulla CDN
|
|
2
|
+
|
|
3
|
+
Con il **tag** `<script>`, o con i componenti in modalità «Sulla tua CDN» (`src` + `config` come URL), sulla CDN vanno **due file**. Una versione HTML con esempi è nella guida del configuratore (`/docs/#pubblicare`).
|
|
4
|
+
|
|
5
|
+
## I file
|
|
6
|
+
|
|
7
|
+
| File | Da dove si prende | Esempio di indirizzo |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **Il loader** | `dist/ual.min.js` di questo progetto, dopo `npm run build` (circa 25 KB, 10 KB con gzip) | `https://adcdn.tuodominio.net/ual.min.js` |
|
|
10
|
+
| **La configurazione** | il JSON scaricato dal configuratore (scheda *JSON* ▸ *Scarica*), che si chiama `<id-sito>.json` | `https://adcdn.tuodominio.net/config/<id-sito>.json` |
|
|
11
|
+
|
|
12
|
+
Un file di configurazione per ogni sito. Nella scheda *Snippet* del configuratore si impostano dominio, nome del loader e percorso della configurazione: l'indirizzo che mostra è quello esatto e lo snippet generato punta già lì.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
cdn/
|
|
16
|
+
├── ual.min.js ← il loader: lo stesso file per tutti i siti
|
|
17
|
+
└── config/
|
|
18
|
+
├── pizzeria-mario.json ← la configurazione: uno per sito
|
|
19
|
+
└── altro-sito.json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Header
|
|
23
|
+
|
|
24
|
+
| File | Header | Perché |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| Configurazione | `Content-Type: application/json`, `Access-Control-Allow-Origin: *`, `Cache-Control: public, max-age=120` | Il loader la legge con `fetch` da un altro dominio (senza cookie, quindi `*` va bene). Cache breve (1–5 minuti) perché le modifiche arrivino in fretta |
|
|
27
|
+
| Loader | `Content-Type: text/javascript`, cache più lunga; abilita gzip o brotli | Si carica con `<script src>`: nessun CORS |
|
|
28
|
+
|
|
29
|
+
Per il loader conviene un percorso con la versione e un alias per i rilasci urgenti:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
cdn/
|
|
33
|
+
├── v0.1.0/ual.min.js ← cache lunga (public, max-age=31536000, immutable)
|
|
34
|
+
├── ual.min.js ← alias, cache breve (public, max-age=300)
|
|
35
|
+
└── config/
|
|
36
|
+
└── pizzeria-mario.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Cosa non serve
|
|
40
|
+
|
|
41
|
+
- `index.mjs`, `react.mjs`, `vue.mjs` e gli altri file di `dist/` diversi da `ual.min.js`: sono per il pacchetto npm.
|
|
42
|
+
- `dist/ual.js` (non minificato): opzionale, utile solo per leggere il codice nel browser durante un debug.
|
|
43
|
+
- Lo schema JSON, la documentazione e i tipi.
|
|
44
|
+
|
|
45
|
+
## Casi particolari
|
|
46
|
+
|
|
47
|
+
- **Pacchetto con la config nel codice** (`defineConfig`, componente senza `src`): sulla CDN non serve niente, UAL e configurazione stanno nel bundle del sito.
|
|
48
|
+
- **Solo gli ad server esistenti** (adapter `iframe`, vedi `ad-server-esistenti.md`): valgono gli stessi due file; gli id e gli URL stanno già nella configurazione.
|
|
49
|
+
- **Immagini delle creatività house** (es. `https://cdn.tuodominio.net/promo/728x90.webp`): sono un terzo tipo di file, che si carica dove si vuole e si indica nella configurazione. Non fanno parte di UAL e non servono header CORS (vengono mostrate in un iframe isolato).
|
|
50
|
+
|
|
51
|
+
## Pubblicare e aggiornare
|
|
52
|
+
|
|
53
|
+
1. Scarica il JSON dal configuratore.
|
|
54
|
+
2. Caricalo sulla CDN all'indirizzo indicato in *Snippet*; carica anche `ual.min.js` (una volta, o a ogni nuova versione del loader).
|
|
55
|
+
3. Incolla nel sito lo snippet (o il codice per il framework).
|
|
56
|
+
4. **Per cambiare provider, slot o regole:** modifica nel configuratore, riscarica il JSON e caricalo di nuovo **allo stesso indirizzo**. I siti lo prendono alla visita successiva, dopo la scadenza della cache.
|
|
57
|
+
|
|
58
|
+
Nelle SPA con il tag va chiamato `ual.pageView()` a ogni cambio di pagina (i componenti lo fanno da soli). Prima di sostituire la configurazione di un sito vero, pubblica con un ID di prova e controlla il registro con `?ual_debug=1`.
|
package/docs/esempio.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Esempio completo: sito cliente con ad server proprio, AdSense e autopromo
|
|
2
|
+
|
|
3
|
+
## 1. Sul sito del cliente
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<head>
|
|
7
|
+
<script async src="https://adcdn.tuodominio.net/ual.min.js"
|
|
8
|
+
data-config="https://adcdn.tuodominio.net/config/pizzeria-mario.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. Configurazione (`pizzeria-mario.json` su CDN)
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"site": "pizzeria-mario",
|
|
23
|
+
"consent": { "mode": "tcf", "requiredPurposes": [1] },
|
|
24
|
+
"targeting": { "categoria": "food", "citta": "milano" },
|
|
25
|
+
|
|
26
|
+
"providers": {
|
|
27
|
+
"mio": { "adapter": "json", "endpoint": "https://ads.tuodominio.net/v1/decide" },
|
|
28
|
+
"adsense": { "adapter": "adsense", "client": "ca-pub-1111111111", "consent": "required" },
|
|
29
|
+
"promo": { "adapter": "house", "creatives": [
|
|
30
|
+
{ "type": "image", "src": "https://adcdn.tuodominio.net/promo/728x90.webp",
|
|
31
|
+
"width": 728, "height": 90, "clickUrl": "https://horbit.it" },
|
|
32
|
+
{ "type": "image", "src": "https://adcdn.tuodominio.net/promo/320x100.webp",
|
|
33
|
+
"width": 320, "height": 100, "clickUrl": "https://horbit.it" }
|
|
34
|
+
]}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
"slots": {
|
|
38
|
+
"top": {
|
|
39
|
+
"label": "Pubblicità", "minHeight": 90, "lazy": false,
|
|
40
|
+
"sizeMapping": [
|
|
41
|
+
{ "minWidth": 0, "sizes": [[320, 100]] },
|
|
42
|
+
{ "minWidth": 768, "sizes": [[728, 90]] }
|
|
43
|
+
],
|
|
44
|
+
"providers": [{ "id": "mio", "params": { "zona": 10 } }, "promo"]
|
|
45
|
+
},
|
|
46
|
+
"sidebar": {
|
|
47
|
+
"sizes": [[300, 250]],
|
|
48
|
+
"providers": [{ "id": "mio", "params": { "zona": 11 } },
|
|
49
|
+
{ "id": "adsense", "params": { "slot": "2222222222" } }]
|
|
50
|
+
},
|
|
51
|
+
"bottom": {
|
|
52
|
+
"sizes": [[728, 90]],
|
|
53
|
+
"providers": [{ "id": "mio", "params": { "zona": 12 } }]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
"pageRules": [
|
|
58
|
+
{ "match": "^/menu", "disable": ["bottom"], "targeting": { "sezione": "menu" } }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 3. Flusso su una visita desktop alla home
|
|
64
|
+
|
|
65
|
+
1. Il loader legge la config e attende la CMP.
|
|
66
|
+
2. `top` e `sidebar` sono nel viewport e partono insieme con **una sola** chiamata:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
POST https://ads.tuodominio.net/v1/decide
|
|
70
|
+
{
|
|
71
|
+
"site": "pizzeria-mario",
|
|
72
|
+
"page": { "url": "https://pizzeriamario.it/", "path": "/" },
|
|
73
|
+
"device": "desktop",
|
|
74
|
+
"targeting": { "categoria": "food", "citta": "milano" },
|
|
75
|
+
"consent": { "gdprApplies": true, "tcString": "CQ…", "granted": true },
|
|
76
|
+
"slots": [
|
|
77
|
+
{ "id": "top#1", "name": "top", "sizes": [[728,90]], "params": { "zona": 10 } },
|
|
78
|
+
{ "id": "sidebar#2", "name": "sidebar", "sizes": [[300,250]], "params": { "zona": 11 } }
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
3. Il server ha una campagna solo per `top`:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{ "ads": [ {
|
|
87
|
+
"slotId": "top#1", "type": "image",
|
|
88
|
+
"src": "https://adcdn.tuodominio.net/c/birra-xyz-728.webp",
|
|
89
|
+
"width": 728, "height": 90,
|
|
90
|
+
"clickUrl": "https://ads.tuodominio.net/c?t=eyJ…",
|
|
91
|
+
"trackers": {
|
|
92
|
+
"impression": ["https://ads.tuodominio.net/i?t=eyJ…"],
|
|
93
|
+
"viewable": ["https://ads.tuodominio.net/v?t=eyJ…"]
|
|
94
|
+
}
|
|
95
|
+
} ] }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
4. Risultato:
|
|
99
|
+
- **top**: banner mostrato in iframe isolato, tracker impression; dopo 1 s visibile al 50% parte il tracker viewable.
|
|
100
|
+
- **sidebar**: vuoto sul tuo server → AdSense del cliente (caricato solo perché il consenso c'è).
|
|
101
|
+
- **bottom**: richiesto solo quando l'utente scorre vicino; se vuoto, lo slot si nasconde.
|
|
102
|
+
- **/menu**: `bottom` non viene caricato, il targeting include `sezione: menu`.
|
|
103
|
+
|
|
104
|
+
## 4. Endpoint `decide` minimo (Node/Fastify)
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
import Fastify from 'fastify';
|
|
108
|
+
const app = Fastify();
|
|
109
|
+
|
|
110
|
+
// CORS: una POST con Content-Type JSON da un altro dominio fa partire prima un preflight OPTIONS.
|
|
111
|
+
// Senza questi header e senza la route OPTIONS il browser blocca tutte le richieste.
|
|
112
|
+
// ('*' va bene perché l'adapter non invia cookie; con withCredentials serve l'origine esatta.)
|
|
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
|
+
// segnaposto: selezione del banner nel tuo ad server
|
|
128
|
+
const banner = await mioAdServer.pick({ site, zona: s.params.zona, sizes: s.sizes, targeting });
|
|
129
|
+
if (!banner) continue; // vuoto -> provider successivo
|
|
130
|
+
|
|
131
|
+
const t = sign({ b: banner.id, s: site, z: s.params.zona }); // segnaposto: token HMAC
|
|
132
|
+
ads.push({
|
|
133
|
+
slotId: s.id, type: 'image', src: banner.imageUrl,
|
|
134
|
+
width: banner.w, height: banner.h,
|
|
135
|
+
clickUrl: `https://ads.tuodominio.net/c?t=${t}`,
|
|
136
|
+
trackers: {
|
|
137
|
+
impression: [`https://ads.tuodominio.net/i?t=${t}`],
|
|
138
|
+
viewable: [`https://ads.tuodominio.net/v?t=${t}`],
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return ads.length ? { ads } : reply.code(204).send();
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`/c`, `/i`, `/v` verificano il token e registrano l'evento; `/c` fa redirect alla landing.
|
|
147
|
+
|
|
148
|
+
## 5. Eventi lato pagina
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
window.ual = window.ual || { cmd: [] };
|
|
152
|
+
ual.cmd.push(u => {
|
|
153
|
+
u.on('slotRendered', e => console.log('mostrato', e.slot, 'da', e.provider));
|
|
154
|
+
u.on('slotEmpty', e => console.log('vuoto', e.slot));
|
|
155
|
+
});
|
|
156
|
+
```
|