universal-ad-loader 0.1.0 → 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.
@@ -1,45 +1,45 @@
1
- # Collegare gli ad server e gli scambi banner esistenti
1
+ # Connecting existing ad servers and banner exchanges
2
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.
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
4
 
5
- > Esiste anche una versione HTML con esempi apribili nel configuratore: `ual-configurator`, pagina `/docs/` (sezione "I tuoi ad server").
5
+ > There is also an HTML version with examples that open in the configurator: `ual-configurator`, `/docs/` page ("I tuoi ad server" section).
6
6
 
7
- ## I sistemi
7
+ ## The systems
8
8
 
9
- | Progetto | Cosa fa | Endpoint pubblici | Quando è "vuoto" |
9
+ | Project | What it does | Public endpoints | When it is "empty" |
10
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 |
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
16
 
17
- Le riscritture Next e il PHP legacy hanno la stessa logica (rotazione, crediti, conteggio click).
17
+ The Next rewrites and the legacy PHP share the same logic (rotation, credits, click counting).
18
18
 
19
- ## Configurazione
19
+ ## Configuration
20
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.
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
22
 
23
- | Sistema | URL | `params` | Sandbox |
23
+ | System | URL | `params` | Sandbox |
24
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 |
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
30
 
31
31
  ```json
32
32
  {
33
- "site": "mio-sito",
33
+ "site": "my-site",
34
34
  "consent": { "mode": "tcf" },
35
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}",
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
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}",
40
+ "exch_next": { "adapter": "iframe", "consent": "contextual", "url": "https://exchange2.yourdomain.net/api/work/{params.user}",
41
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}" }
42
+ "exch_legacy": { "adapter": "iframe", "consent": "contextual", "url": "https://banner.yourdomain.net/display.php?id={params.host}" }
43
43
  },
44
44
  "slots": {
45
45
  "top": { "sizes": [[728, 90]], "minHeight": 90, "providers": [{ "id": "adm_next", "params": { "unit": 3 } }] },
@@ -51,28 +51,28 @@ Sostituisci i domini con i tuoi. Le dimensioni dello slot devono coincidere con
51
51
  }
52
52
  ```
53
53
 
54
- È l'esempio "I tuoi ad server esistenti" del configuratore: si apre da `?esempio=ad-server-esistenti`.
54
+ This is the configurator's "Your existing ad servers" example: it opens from `?esempio=ad-server-esistenti`.
55
55
 
56
- ## Limiti
56
+ ## Limits
57
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).
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
65
 
66
- ## Come avere il fallback dopo un ad server
66
+ ## How to get a fallback after an ad server
67
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`.
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
69
 
70
- Per AdManager (Next) servirebbe un endpoint tipo `POST /api/ual/decide` che:
70
+ For AdManager (Next) this would need an endpoint like `POST /api/ual/decide` that:
71
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.
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
77
 
78
- Non è stato implementato: i progetti esistenti non sono stati modificati.
78
+ It has not been implemented: the existing projects have not been modified.
package/docs/cdn.md CHANGED
@@ -1,58 +1,58 @@
1
- # Pubblicare UAL: i file da mettere sulla CDN
1
+ # Publishing UAL: the files to put on the CDN
2
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`).
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
4
 
5
- ## I file
5
+ ## The files
6
6
 
7
- | File | Da dove si prende | Esempio di indirizzo |
7
+ | File | Where it comes from | Example address |
8
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` |
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
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ì.
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
13
 
14
14
  ```
15
15
  cdn/
16
- ├── ual.min.js ← il loader: lo stesso file per tutti i siti
16
+ ├── ual.min.js ← the loader: the same file for all sites
17
17
  └── config/
18
- ├── pizzeria-mario.json ← la configurazione: uno per sito
19
- └── altro-sito.json
18
+ ├── mario-pizzeria.json ← the configuration: one per site
19
+ └── another-site.json
20
20
  ```
21
21
 
22
- ## Header
22
+ ## Headers
23
23
 
24
- | File | Header | Perché |
24
+ | File | Headers | Why |
25
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 |
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
28
 
29
- Per il loader conviene un percorso con la versione e un alias per i rilasci urgenti:
29
+ For the loader it is best to use a versioned path plus an alias for urgent releases:
30
30
 
31
31
  ```
32
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)
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
35
  └── config/
36
- └── pizzeria-mario.json
36
+ └── mario-pizzeria.json
37
37
  ```
38
38
 
39
- ## Cosa non serve
39
+ ## What is not needed
40
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.
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
44
 
45
- ## Casi particolari
45
+ ## Special cases
46
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).
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
50
 
51
- ## Pubblicare e aggiornare
51
+ ## Publishing and updating
52
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.
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
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`.
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`.
package/docs/esempio.md CHANGED
@@ -1,11 +1,11 @@
1
- # Esempio completo: sito cliente con ad server proprio, AdSense e autopromo
1
+ # Complete example: client site with its own ad server, AdSense and house promos
2
2
 
3
- ## 1. Sul sito del cliente
3
+ ## 1. On the client's site
4
4
 
5
5
  ```html
6
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>
7
+ <script async src="https://adcdn.yourdomain.net/ual.min.js"
8
+ data-config="https://adcdn.yourdomain.net/config/mario-pizzeria.json"></script>
9
9
  </head>
10
10
  <body>
11
11
  <div data-ad-slot="top"></div>
@@ -15,101 +15,101 @@
15
15
  </body>
16
16
  ```
17
17
 
18
- ## 2. Configurazione (`pizzeria-mario.json` su CDN)
18
+ ## 2. Configuration (`mario-pizzeria.json` on the CDN)
19
19
 
20
20
  ```json
21
21
  {
22
- "site": "pizzeria-mario",
22
+ "site": "mario-pizzeria",
23
23
  "consent": { "mode": "tcf", "requiredPurposes": [1] },
24
- "targeting": { "categoria": "food", "citta": "milano" },
24
+ "targeting": { "category": "food", "city": "milan" },
25
25
 
26
26
  "providers": {
27
- "mio": { "adapter": "json", "endpoint": "https://ads.tuodominio.net/v1/decide" },
27
+ "mine": { "adapter": "json", "endpoint": "https://ads.yourdomain.net/v1/decide" },
28
28
  "adsense": { "adapter": "adsense", "client": "ca-pub-1111111111", "consent": "required" },
29
29
  "promo": { "adapter": "house", "creatives": [
30
- { "type": "image", "src": "https://adcdn.tuodominio.net/promo/728x90.webp",
30
+ { "type": "image", "src": "https://adcdn.yourdomain.net/promo/728x90.webp",
31
31
  "width": 728, "height": 90, "clickUrl": "https://horbit.it" },
32
- { "type": "image", "src": "https://adcdn.tuodominio.net/promo/320x100.webp",
32
+ { "type": "image", "src": "https://adcdn.yourdomain.net/promo/320x100.webp",
33
33
  "width": 320, "height": 100, "clickUrl": "https://horbit.it" }
34
34
  ]}
35
35
  },
36
36
 
37
37
  "slots": {
38
38
  "top": {
39
- "label": "Pubblicità", "minHeight": 90, "lazy": false,
39
+ "label": "Advertising", "minHeight": 90, "lazy": false,
40
40
  "sizeMapping": [
41
41
  { "minWidth": 0, "sizes": [[320, 100]] },
42
42
  { "minWidth": 768, "sizes": [[728, 90]] }
43
43
  ],
44
- "providers": [{ "id": "mio", "params": { "zona": 10 } }, "promo"]
44
+ "providers": [{ "id": "mine", "params": { "zone": 10 } }, "promo"]
45
45
  },
46
46
  "sidebar": {
47
47
  "sizes": [[300, 250]],
48
- "providers": [{ "id": "mio", "params": { "zona": 11 } },
48
+ "providers": [{ "id": "mine", "params": { "zone": 11 } },
49
49
  { "id": "adsense", "params": { "slot": "2222222222" } }]
50
50
  },
51
51
  "bottom": {
52
52
  "sizes": [[728, 90]],
53
- "providers": [{ "id": "mio", "params": { "zona": 12 } }]
53
+ "providers": [{ "id": "mine", "params": { "zone": 12 } }]
54
54
  }
55
55
  },
56
56
 
57
57
  "pageRules": [
58
- { "match": "^/menu", "disable": ["bottom"], "targeting": { "sezione": "menu" } }
58
+ { "match": "^/menu", "disable": ["bottom"], "targeting": { "section": "menu" } }
59
59
  ]
60
60
  }
61
61
  ```
62
62
 
63
- ## 3. Flusso su una visita desktop alla home
63
+ ## 3. Flow on a desktop visit to the home page
64
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:
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
67
 
68
68
  ```json
69
- POST https://ads.tuodominio.net/v1/decide
69
+ POST https://ads.yourdomain.net/v1/decide
70
70
  {
71
- "site": "pizzeria-mario",
72
- "page": { "url": "https://pizzeriamario.it/", "path": "/" },
71
+ "site": "mario-pizzeria",
72
+ "page": { "url": "https://mariopizzeria.it/", "path": "/" },
73
73
  "device": "desktop",
74
- "targeting": { "categoria": "food", "citta": "milano" },
74
+ "targeting": { "category": "food", "city": "milan" },
75
75
  "consent": { "gdprApplies": true, "tcString": "CQ…", "granted": true },
76
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 } }
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
79
  ]
80
80
  }
81
81
  ```
82
82
 
83
- 3. Il server ha una campagna solo per `top`:
83
+ 3. The server has a campaign only for `top`:
84
84
 
85
85
  ```json
86
86
  { "ads": [ {
87
87
  "slotId": "top#1", "type": "image",
88
- "src": "https://adcdn.tuodominio.net/c/birra-xyz-728.webp",
88
+ "src": "https://adcdn.yourdomain.net/c/beer-xyz-728.webp",
89
89
  "width": 728, "height": 90,
90
- "clickUrl": "https://ads.tuodominio.net/c?t=eyJ…",
90
+ "clickUrl": "https://ads.yourdomain.net/c?t=eyJ…",
91
91
  "trackers": {
92
- "impression": ["https://ads.tuodominio.net/i?t=eyJ…"],
93
- "viewable": ["https://ads.tuodominio.net/v?t=eyJ…"]
92
+ "impression": ["https://ads.yourdomain.net/i?t=eyJ…"],
93
+ "viewable": ["https://ads.yourdomain.net/v?t=eyJ…"]
94
94
  }
95
95
  } ] }
96
96
  ```
97
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`.
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
103
 
104
- ## 4. Endpoint `decide` minimo (Node/Fastify)
104
+ ## 4. Minimal `decide` endpoint (Node/Fastify)
105
105
 
106
106
  ```js
107
107
  import Fastify from 'fastify';
108
108
  const app = Fastify();
109
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.)
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
113
  app.addHook('onRequest', async (req, reply) => {
114
114
  reply.header('Access-Control-Allow-Origin', '*');
115
115
  reply.header('Access-Control-Allow-Methods', 'POST, OPTIONS');
@@ -124,18 +124,18 @@ app.post('/v1/decide', async (req, reply) => {
124
124
 
125
125
  const ads = [];
126
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
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
130
 
131
- const t = sign({ b: banner.id, s: site, z: s.params.zona }); // segnaposto: token HMAC
131
+ const t = sign({ b: banner.id, s: site, z: s.params.zone }); // placeholder: HMAC token
132
132
  ads.push({
133
133
  slotId: s.id, type: 'image', src: banner.imageUrl,
134
134
  width: banner.w, height: banner.h,
135
- clickUrl: `https://ads.tuodominio.net/c?t=${t}`,
135
+ clickUrl: `https://ads.yourdomain.net/c?t=${t}`,
136
136
  trackers: {
137
- impression: [`https://ads.tuodominio.net/i?t=${t}`],
138
- viewable: [`https://ads.tuodominio.net/v?t=${t}`],
137
+ impression: [`https://ads.yourdomain.net/i?t=${t}`],
138
+ viewable: [`https://ads.yourdomain.net/v?t=${t}`],
139
139
  },
140
140
  });
141
141
  }
@@ -143,14 +143,14 @@ app.post('/v1/decide', async (req, reply) => {
143
143
  });
144
144
  ```
145
145
 
146
- `/c`, `/i`, `/v` verificano il token e registrano l'evento; `/c` fa redirect alla landing.
146
+ `/c`, `/i`, `/v` verify the token and record the event; `/c` redirects to the landing page.
147
147
 
148
- ## 5. Eventi lato pagina
148
+ ## 5. Page-side events
149
149
 
150
150
  ```js
151
151
  window.ual = window.ual || { cmd: [] };
152
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));
153
+ u.on('slotRendered', e => console.log('shown', e.slot, 'by', e.provider));
154
+ u.on('slotEmpty', e => console.log('empty', e.slot));
155
155
  });
156
156
  ```