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.
- package/CHANGELOG.md +20 -12
- package/README.md +104 -105
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/ual.js +1 -1
- package/dist/ual.min.js +1 -1
- package/docs/ad-server-esistenti.md +43 -43
- package/docs/cdn.md +32 -32
- package/docs/esempio.md +49 -49
- package/docs/libreria.md +50 -50
- package/package.json +8 -1
- package/docs/pubblicazione-npm.md +0 -114
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Connecting existing ad servers and banner exchanges
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
>
|
|
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
|
-
##
|
|
7
|
+
## The systems
|
|
8
8
|
|
|
9
|
-
|
|
|
9
|
+
| Project | What it does | Public endpoints | When it is "empty" |
|
|
10
10
|
|---|---|---|---|
|
|
11
|
-
| `admanager-next` | Ad server:
|
|
12
|
-
| `bannerpro-next` |
|
|
13
|
-
| `banner-next` |
|
|
14
|
-
| `banner/` (PHP
|
|
15
|
-
| `banner/admanager/` (PHP
|
|
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
|
-
|
|
17
|
+
The Next rewrites and the legacy PHP share the same logic (rotation, credits, click counting).
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## Configuration
|
|
20
20
|
|
|
21
|
-
|
|
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
|
-
|
|
|
23
|
+
| System | URL | `params` | Sandbox |
|
|
24
24
|
|---|---|---|---|
|
|
25
|
-
| AdManager (Next) | `https://…/api/iframe/{params.unit}` | `{"unit": id
|
|
26
|
-
| AdManager (legacy) | `https://…/banner/admanager/iframe.php?id={params.unit}` | `{"unit": id
|
|
27
|
-
| Banner Exchange Pro (Next) | `https://…/api/display/{params.host}` | `{"host": id
|
|
28
|
-
| Banner Exchange (Next) | `https://…/api/work/{params.user}` | `{"user": "username"}` (
|
|
29
|
-
|
|
|
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": "
|
|
33
|
+
"site": "my-site",
|
|
34
34
|
"consent": { "mode": "tcf" },
|
|
35
35
|
"providers": {
|
|
36
|
-
"adm_next": { "adapter": "iframe", "consent": "contextual", "url": "https://admanager.
|
|
37
|
-
"adm_legacy": { "adapter": "iframe", "consent": "contextual", "url": "https://banner.
|
|
38
|
-
"exch_pro": { "adapter": "iframe", "consent": "contextual", "url": "https://
|
|
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://
|
|
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.
|
|
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
|
-
|
|
54
|
+
This is the configurator's "Your existing ad servers" example: it opens from `?esempio=ad-server-esistenti`.
|
|
55
55
|
|
|
56
|
-
##
|
|
56
|
+
## Limits
|
|
57
57
|
|
|
58
|
-
- **
|
|
59
|
-
-
|
|
60
|
-
- **
|
|
61
|
-
- **
|
|
62
|
-
- **
|
|
63
|
-
-
|
|
64
|
-
- **
|
|
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
|
-
##
|
|
66
|
+
## How to get a fallback after an ad server
|
|
67
67
|
|
|
68
|
-
|
|
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
|
-
|
|
70
|
+
For AdManager (Next) this would need an endpoint like `POST /api/ual/decide` that:
|
|
71
71
|
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
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
|
-
|
|
78
|
+
It has not been implemented: the existing projects have not been modified.
|
package/docs/cdn.md
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Publishing UAL: the files to put on the CDN
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## The files
|
|
6
6
|
|
|
7
|
-
| File |
|
|
7
|
+
| File | Where it comes from | Example address |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| **
|
|
10
|
-
| **
|
|
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
|
-
|
|
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 ←
|
|
16
|
+
├── ual.min.js ← the loader: the same file for all sites
|
|
17
17
|
└── config/
|
|
18
|
-
├── pizzeria
|
|
19
|
-
└──
|
|
18
|
+
├── mario-pizzeria.json ← the configuration: one per site
|
|
19
|
+
└── another-site.json
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Headers
|
|
23
23
|
|
|
24
|
-
| File |
|
|
24
|
+
| File | Headers | Why |
|
|
25
25
|
|---|---|---|
|
|
26
|
-
|
|
|
27
|
-
| Loader | `Content-Type: text/javascript`, cache
|
|
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
|
-
|
|
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
|
|
34
|
-
├── ual.min.js ← alias, cache
|
|
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
|
|
36
|
+
└── mario-pizzeria.json
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## What is not needed
|
|
40
40
|
|
|
41
|
-
- `index.mjs`, `react.mjs`, `vue.mjs`
|
|
42
|
-
- `dist/ual.js` (
|
|
43
|
-
-
|
|
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
|
-
##
|
|
45
|
+
## Special cases
|
|
46
46
|
|
|
47
|
-
- **
|
|
48
|
-
- **
|
|
49
|
-
- **
|
|
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
|
-
##
|
|
51
|
+
## Publishing and updating
|
|
52
52
|
|
|
53
|
-
1.
|
|
54
|
-
2.
|
|
55
|
-
3.
|
|
56
|
-
4. **
|
|
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
|
-
|
|
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
|
-
#
|
|
1
|
+
# Complete example: client site with its own ad server, AdSense and house promos
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1. On the client's site
|
|
4
4
|
|
|
5
5
|
```html
|
|
6
6
|
<head>
|
|
7
|
-
<script async src="https://adcdn.
|
|
8
|
-
data-config="https://adcdn.
|
|
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.
|
|
18
|
+
## 2. Configuration (`mario-pizzeria.json` on the CDN)
|
|
19
19
|
|
|
20
20
|
```json
|
|
21
21
|
{
|
|
22
|
-
"site": "pizzeria
|
|
22
|
+
"site": "mario-pizzeria",
|
|
23
23
|
"consent": { "mode": "tcf", "requiredPurposes": [1] },
|
|
24
|
-
"targeting": { "
|
|
24
|
+
"targeting": { "category": "food", "city": "milan" },
|
|
25
25
|
|
|
26
26
|
"providers": {
|
|
27
|
-
"
|
|
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.
|
|
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.
|
|
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": "
|
|
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": "
|
|
44
|
+
"providers": [{ "id": "mine", "params": { "zone": 10 } }, "promo"]
|
|
45
45
|
},
|
|
46
46
|
"sidebar": {
|
|
47
47
|
"sizes": [[300, 250]],
|
|
48
|
-
"providers": [{ "id": "
|
|
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": "
|
|
53
|
+
"providers": [{ "id": "mine", "params": { "zone": 12 } }]
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
|
|
57
57
|
"pageRules": [
|
|
58
|
-
{ "match": "^/menu", "disable": ["bottom"], "targeting": { "
|
|
58
|
+
{ "match": "^/menu", "disable": ["bottom"], "targeting": { "section": "menu" } }
|
|
59
59
|
]
|
|
60
60
|
}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
## 3.
|
|
63
|
+
## 3. Flow on a desktop visit to the home page
|
|
64
64
|
|
|
65
|
-
1.
|
|
66
|
-
2. `top`
|
|
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.
|
|
69
|
+
POST https://ads.yourdomain.net/v1/decide
|
|
70
70
|
{
|
|
71
|
-
"site": "pizzeria
|
|
72
|
-
"page": { "url": "https://
|
|
71
|
+
"site": "mario-pizzeria",
|
|
72
|
+
"page": { "url": "https://mariopizzeria.it/", "path": "/" },
|
|
73
73
|
"device": "desktop",
|
|
74
|
-
"targeting": { "
|
|
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": { "
|
|
78
|
-
{ "id": "sidebar#2", "name": "sidebar", "sizes": [[300,250]], "params": { "
|
|
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.
|
|
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.
|
|
88
|
+
"src": "https://adcdn.yourdomain.net/c/beer-xyz-728.webp",
|
|
89
89
|
"width": 728, "height": 90,
|
|
90
|
-
"clickUrl": "https://ads.
|
|
90
|
+
"clickUrl": "https://ads.yourdomain.net/c?t=eyJ…",
|
|
91
91
|
"trackers": {
|
|
92
|
-
"impression": ["https://ads.
|
|
93
|
-
"viewable": ["https://ads.
|
|
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.
|
|
99
|
-
- **top**: banner
|
|
100
|
-
- **sidebar**:
|
|
101
|
-
- **bottom**:
|
|
102
|
-
- **/menu**: `bottom`
|
|
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.
|
|
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:
|
|
111
|
-
//
|
|
112
|
-
// ('*'
|
|
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
|
-
//
|
|
128
|
-
const banner = await
|
|
129
|
-
if (!banner) continue; //
|
|
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.
|
|
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.
|
|
135
|
+
clickUrl: `https://ads.yourdomain.net/c?t=${t}`,
|
|
136
136
|
trackers: {
|
|
137
|
-
impression: [`https://ads.
|
|
138
|
-
viewable: [`https://ads.
|
|
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`
|
|
146
|
+
`/c`, `/i`, `/v` verify the token and record the event; `/c` redirects to the landing page.
|
|
147
147
|
|
|
148
|
-
## 5.
|
|
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('
|
|
154
|
-
u.on('slotEmpty', e => console.log('
|
|
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
|
```
|