# Building high impact creatives for Aeon Ad Manager

This guide is for the people and coding agents that build ads for sites running Aeon Ad Manager. It covers:
- how each format looks and behaves on the page;
- what to deliver for each format;
- how an HTML5 creative talks to the page.

How to upload the result is described step by step in the agent brief (`AGENT-BRIEF.md` in the starter kit; in the plugin's source it is `kit/AGENT-BRIEF.md`, next to the full REST reference in `docs/rest-api.md`).

The starter kit (`kit/`) contains:
- a template per format;
- the creative SDK;
- a simulator that runs the sites' own ad script;
- a checker that applies the same rules as the server.

## How it fits together

- **Ad, creative, placement.** An ad is shown in a placement (an area on the site). What the reader sees is the ad's creative.
  - A creative never changes. A new version is a new creative.
  - The format is the creative's `type`, and its settings are in `config`.
- **The page draws the frame, you draw the content.** The site draws everything around the creative:
  - the "Advertisement" bars, the arrow, the close buttons and the reel's dialog;
  - the window the creative is seen through.
  
  A creative is either images (uploaded to the WordPress media library) or an **HTML5 package** (a ZIP), which always comes with a poster image.
- **HTML5 creatives run in a locked frame.** The frame is a sandboxed iframe, with no access to the site, no network and no navigation of its own.
  - The page tells the creative what it needs: size, scroll position, visibility and slide.
  - The creative asks the page to open a link (`AeonCreative.click()`). That keeps clicks counted and the site safe.
- **Nothing is counted in a preview.** A preview link shows a creative on the real site before it is active. The ad stays a draft.

## The formats

| Format | Where | HTML5 | In short |
|---|---|---|---|
| `topscroll` | top of every page, above the masthead | yes | a full-width scene the page slides up over |
| `midscroll` | in the article text | yes | a window in the text onto a still, full-screen scene |
| `skin` | both sides of the site, screens ≥ 1280 px | yes | artwork either side of the content, plus an optional band above it |
| `reel` | in the article text or a widget area | yes | a row of portrait cards that opens a full-screen story |
| `display` | widget areas and the article text | yes | a box of a fixed size (300×250, 300×600 …) or a responsive one |
| `interstitial`, `layer` | over the page | no | an image in a dialog, after a delay or (layer) on exit intent |
| `sticky` | bottom of phones, right edge of desktops | no | a small image that stays on screen |
| `carousel` | widget areas and the article text | no | 2–8 images to swipe through |
| `native` | widget areas and the article text | no | title, text, image and logo in the site's own style |

Sizes below are CSS pixels. `vh` is a share of the screen height: 72 vh is 72 % of it.

### Topscroll

**What the reader sees.**
- The first thing on the page is a scene the full width of the screen:
  - 72 vh tall on desktop;
  - 80 vh on phones (screens under 768 px).
- Under it runs a dark bar, 34 px tall, with "Advertisement · <advertiser>" on the left and "Scroll to continue ↓" on the right.
- The scene is fixed to the screen. As the reader scrolls, the site slides up over it, like a curtain, until it is gone.
- The page draws a round white arrow button (48 px, centred, 24 px from the bottom of the scene) that scrolls to the site. It disappears once the reader has scrolled.

**Design for it.**
- **Desktop:**
  - The scene is wide and fairly low: 1440×648 on a 1440×900 screen, 1920×778 on a full-HD screen.
  - Keep the message in the middle 1200×560 px.
  - Keep the bottom centre (about 120×100 px) free: the arrow sits there.
- **Phones:** the scene is portrait, about 390×675 on an iPhone. Keep the message in the middle 80 % of the width and height.
- The scene starts at the top of the page, so the reader sees all of it first. Use scroll progress for an exit effect, not to reveal the message.

**Deliver.**
- `attachment_id`: the desktop poster, 1920×800 (it is cropped to fill the scene, centred).
- `mobile_id` (recommended): the phone poster, 1080×1800.
- `alt`.
- HTML5 (optional):
  - a package with `formats: ["topscroll"]` and `size: "responsive"`, which fills the scene;
  - optionally a separate phone package (`html5_mobile`), chosen when the page loads on a screen under 768 px.

**Scroll progress.** 0 at the top of the page, rising to 1 when the scene has scrolled away.

### Midscroll

**What the reader sees.**
- In the middle of an article, a thin bar "Advertisement · <advertiser>", then a window, then a bar "The article continues below ↓", with 28 px above and below.
- The window is 70 vh tall on desktop and 80 vh on phones.
- Behind it is a still scene the size of the whole screen. The window moves with the text and shows the part of the scene behind it, so the reader passes over the creative as if through a hole in the page.

**Two widths** (`config.width`):
- `full` (default): edge to edge across the screen, even from inside a narrow article column.
- `content`: as wide as the site's frame (the theme's `--aeon-ad-site-width`, 1310 px by default), centred.
- Phones always get the full width.

**Design for it.**
- The scene is the whole screen (1440×900, 1920×1080, 390×844 …), but at any moment the window shows only a band of it. When the window is centred on the screen, it shows the middle 70 % (80 % on phones).
- Keep the message in the middle 60 % of the height and the middle 1200 px of the width.
- Fill the rest with background: it is seen at the start and end of the pass.

**Deliver.**
- `attachment_id`: 1920×1080, cropped to fill the screen.
- `mobile_id` (recommended): 1080×1920 portrait.
- `alt`, `width`.
- HTML5 (optional): `formats: ["midscroll"]` and `size: "responsive"`. The frame is as tall as the screen and as wide as the window: the whole screen for `full`, the site's frame for `content` (1100 × 900 on a 1440 × 900 screen with a 1100 px site). Read the size from `aeon:init` (`slot.width`) or from the frame's own `innerWidth`, and lay out again when it changes.

**Scroll progress.**
- 0 when the window's top enters at the bottom of the screen, 1 when its bottom leaves at the top.
- 0.5 is about when the window is centred. A good effect builds from about 0.2 and is complete by 0.7.

### Skin

**What the reader sees.**
- The site's content stays in the middle, and the advertiser's artwork fills the space on both sides from the top of the screen to the bottom, staying put while the page scrolls.
- An optional band runs above the content at the top of the page and scrolls away with it.
- **Skins appear only on screens 1280 px or wider.** On narrower screens nothing is drawn and nothing is counted.
- **The site's theme must make room for a skin:** it sets the content width (`--aeon-skin-content`) and keeps its content inside it while the skin is shown (`html.aeon-ad-skin-on`). Ask the newspaper which pages carry skins; a page whose layout is wider than that width has its edges covered.

**How much of the sides is seen.** Each side is (screen width − content width) / 2. With the default content width of 1100 px:
- 90 px at 1280;
- 170 px at 1440;
- 410 px at 1920.

Themes can narrow their content while a skin is shown.

**Design for it.**
- Put the logo and a short line in the **160 px next to the content**: that is all a 1440 screen shows.
- Further out is seen only on wide screens. Use it for atmosphere, not for the message.
- Many sites run their header and menus across the full width. At the top of the page they cover the upper part of the sides, which are seen whole once the reader scrolls. Put the message in the lower half of the side.
- Image sides are scaled to the screen height and pinned to the content edge, so their copy keeps its size.

**Deliver (images).**
- `left_id` and `right_id`: each 1100×2560, with the copy in the 240 px nearest the content. WordPress scales any image larger than 2560 px down to 2560, so a 1200×2800 image becomes 1097×2560; either works.
- `top_id` (optional): a band 2200×300.

**Deliver (HTML5).**
- A package with `formats: ["skin"]` and `size: "responsive"` (required). Set `config.top_height` (0–400 px) for the band above the content.
- `left_id` and `right_id` are still required. They are the poster: they are shown until the creative is ready, under reduced motion with `reducedMotion: "backup"`, or if the creative fails.
- The frame is the whole window, laid over the page but cut away over the content. Only the sides and the band are seen and clickable.
- `aeon:init` gives:
  - `content: {left, right, width}`: where the content column is, in px from the left of the window;
  - `top: {height, offset}`: the band's height, and its distance from the top of the page.
- Every `aeon:scroll` gives `y`, the page's scroll position. Draw the band at `offset − y` so it scrolls away with the page, and keep the sides still.
- Lay out again on every `aeon:init`: it comes again when the window is resized.
- The template `templates/skin` does all of this.

**Scroll progress.** How far down the page the reader is, 0–1. A skin is never paused by scrolling, only when the tab is hidden.

### Reel

**What the reader sees.**
- An optional title, then a row of 3–5 portrait cards that can be swiped sideways.
- A tap opens a dialog on that card:
  - **desktop:** as tall as the screen allows (up to 940 px), 9:16;
  - **phones:** full screen.
- The dialog's header shows "Advertisement · <advertiser>", back and next arrows, "2 of 4" and a close button. The arrow keys also move between slides.
- A call-to-action button (`cta`) runs under the slide when the ad has a destination.

**Deliver (images).**
- `slides`: 3–5 of `{attachment_id, alt}`, each 1080×1920.
- `attachment_id`: a backup image, also 1080×1920.
- `title` (optional), `cta` (optional).

**Deliver (HTML5).**
- One package for the whole story, with `formats: ["reel"]` and `size` either `"responsive"` or 9:16 (for example `{width: 1080, height: 1920}`).
- The frame fills the slide area of the dialog.
- `slides` and `attachment_id` are still required: the cards in the row are always the slide images.
- The page sends `aeon:slide {index, count}` when the dialog opens and whenever the reader moves (index starts at 0). Show that slide.
- To move by yourself, for example on a swipe inside the creative, call `AeonCreative.go(index)`. The page moves its counter and answers with `aeon:slide`.
- The creative is paused while the dialog is closed.
- Leave `cta` out of the config when the creative draws its own button, so the dialog does not show two.

**Scroll progress.** The slide as a share of the story: `index / (count − 1)`.

### Display

**What the reader sees.** An ordinary ad box with the label "Advertisement" above it.
- **Fixed size:** the box keeps the creative's ratio and shrinks if the column is narrower (a 300×600 in a 280 px column is 280×560).
- **Responsive:** the box is the column's width, and 250 px tall until the creative asks for a height with `AeonCreative.resize(height)` (50–1200 px).

**Deliver.**
- `attachment_id`: the backup image, at the ad's size.
- HTML5: `formats: ["display"]`, with `size: {width, height}` for a fixed box or `"responsive"`.
- Optionally `html5_mobile` for phones.
- Common sizes: 300×250, 300×600, 320×320 (phones), 970×250, 728×90.

### Image-only formats

| Format | Config | Image |
|---|---|---|
| `interstitial` | `attachment_id`, `alt`, `delay_seconds` (0–120), `cap_per_day` (1–20), `skip_first_view` | 600×500. Shown over a dimmed page with a close button, at most `cap_per_day` times a day per browser |
| `layer` | `attachment_id`, `alt`, `delay_seconds`, `exit_intent` (true/false), `cap_per_day` | 500×500. Like the interstitial; with `exit_intent` it opens when the mouse leaves the top of the window |
| `sticky` | `attachment_id`, `alt` | phones 320×100 along the bottom, desktops 160×600 on the right edge; has a close button |
| `carousel` | `slides`: 2–8 of `{attachment_id, alt}` | 300×250 or the placement's size; moves every 5 s until touched |
| `native` | `title`, `text`, `attachment_id`, `logo_id`, `advertiser`, `label` (`Annons` or `Sponsored`), `cta` | 600×320 and a logo; text is shown as plain text |

Every image may be JPEG, PNG, WebP or GIF and at most 300 KB.

## HTML5 packages

### Files

A ZIP with these at the root (a single wrapping folder is also accepted):
- `manifest.json`: see below.
- `index.html`: the creative.
- `backup.webp`, `backup.png` or `backup.jpg`: at most 200 KB. It is checked but not shown: the poster on the site is the image given in the creative's config.
- The SDK as a local file, for example `assets/aeon-creative.js`, loaded with `<script src="assets/aeon-creative.js"></script>`.
- Your own files, in folders as you like.

```json
{
  "version": 1,
  "name": "Vireo Home – air monitor (midscroll)",
  "size": "responsive",
  "formats": ["midscroll"],
  "exits": { "main": "Discover Vireo", "product": "How it works" },
  "duration": 25,
  "reducedMotion": "static"
}
```

| Field | Rule |
|---|---|
| `version` | the number 1 |
| `name` | a non-empty name |
| `size` | `"responsive"`, or `{ "width": 300, "height": 600 }` (whole numbers). Skin: responsive. Reel: responsive or 9:16 |
| `formats` | one or more of `display`, `topscroll`, `midscroll`, `skin`, `reel`. A creative can only use a package whose list has its format |
| `exits` | names of the places a click can go, each with a label. `main` is required. Names: letters, digits, `-` and `_`, at most 20 characters. The web addresses are not in the package: they are set on the creative |
| `duration` | optional, the length of the animation in seconds, at most 30 |
| `reducedMotion` | optional. `"backup"`: readers who ask for reduced motion see only the poster. `"static"`: the creative is loaded and must show a still state itself |

### Limits

- At most 5 MB packed, 3.5 MB unpacked and 200 files. Aim for under 1 MB on first load.
- Allowed file types: `html`, `htm`, `css`, `js`, `mjs`, `json`, `svg`, `png`, `jpg`, `jpeg`, `gif`, `webp`, `avif`, `woff`, `woff2`, `mp4`, `webm` and `txt`.
  - **Fonts must be WOFF or WOFF2**, not TTF or OTF.
  - Archives, server scripts and dot files are refused.
- **Nothing from outside the package.** In `index.html` and in scripts, any `src`, `href`, `url()`, `import`, `fetch` or `XMLHttpRequest` pointing to `http://`, `https://` or `//` is refused, even a plain link.
  - CSS files are not scanned, but the frame blocks remote loads anyway.
- **No network at all, not even to the package's own files.** `fetch` and XHR are blocked by the frame's rules, so put data inline in a script instead of loading a JSON file.
- Use relative paths (`assets/hero.webp`).
  - The site rewrites `src`, `href` and CSS `url()` in HTML and CSS files.
  - It does **not** rewrite `srcset`. Use `src`, CSS, or `AeonCreative.asset('assets/hero@2x.webp')` in a script.
  - Scripts that build file URLs themselves must use `AeonCreative.asset(path)`.

### The SDK

Load `aeon-creative.js` from the package. It gives `window.AeonCreative`:

| Call or field | Use |
|---|---|
| `AeonCreative.on(type, fn)` | listen for a message from the page: `init`, `scroll`, `visibility`, `pause`, `resume`, `slide` (with or without the `aeon:` prefix) |
| `AeonCreative.click(exit)` | ask the page to open an exit (default `main`) in a new tab. Call it from a click or tap handler only |
| `AeonCreative.ready()` | tell the page the creative can be shown. Called automatically at `DOMContentLoaded`; set `window.AEON_MANUAL_READY = true` before the SDK to call it yourself (for example after fonts and images have loaded) |
| `AeonCreative.resize(height)` | responsive display only: ask for a height, 50–1200 px |
| `AeonCreative.go(index)` | reel only: ask for another slide |
| `AeonCreative.asset(path)` | the URL of a package file, for scripts that build URLs |
| `AeonCreative.state` | the latest values: `progress`, `visible`, `paused`, `reducedMotion`, `viewport`, `slot`, `format`, `exits`, `content` and `top` (skin), `y` (skin), `slide` (reel) |

`window.clickTag` is `'#'` for tools that expect it. Never navigate with it: use `AeonCreative.click()`.

### Messages

| Direction | Type | Data |
|---|---|---|
| page → creative | `aeon:init` | `viewport {width, height, dpr}`, `slot {width, height}`, `format`, `exits`, `reducedMotion`. Skin adds `content` and `top`; reel adds `slide` |
| page → creative | `aeon:scroll` | `progress` (0–1), `visible` (the share of the slot on screen, 0–1). Skin adds `y` |
| page → creative | `aeon:visibility` | `visible` (true/false) |
| page → creative | `aeon:pause`, `aeon:resume` | none |
| page → creative | `aeon:slide` | `index`, `count` (reel) |
| creative → page | `aeon:ready` | none |
| creative → page | `aeon:click` | `exit` |
| creative → page | `aeon:resize` | `height` (responsive display) |
| creative → page | `aeon:goto` | `index` (reel) |

**Timing:**
1. The page shows the poster first and loads the creative in an invisible frame.
2. `aeon:init` can arrive as soon as the frame has loaded, even before `ready`, and comes again whenever the window is resized.
3. **The creative must call `ready` within 4 seconds.** Otherwise the frame is removed and the reader keeps the poster.
4. After `ready`, the frame fades in and the poster goes. The page then sends `aeon:visibility`, `aeon:resume` and `aeon:scroll`.
5. `aeon:scroll` comes only while the slot is on screen and the tab is visible, and only when something has changed.
6. `aeon:pause` comes when the slot leaves the screen or the tab is hidden, and `aeon:resume` when it is back.

**Clicks:**
- A click opens only during a real tap or click by the reader, at most once a second.
- An exit that is not in the manifest becomes `main`.
- The creative's `config.exits` must give each exit a web address. An exit without one goes to `main`, and `main` falls back to the creative's `destination_url`.

## Rules for every creative

- **Sound** only after the reader has tapped or clicked the creative. Video starts muted.
- **Animation:**
  - at most 30 seconds and 5 loops, then a still end frame;
  - stop all animation on `aeon:pause` and continue on `aeon:resume`.
- **Reduced motion:** when `reducedMotion` is true in `aeon:init` (or `AeonCreative.state.reducedMotion`), show a still state without movement. Effects tied to scrolling may still follow the scroll, but nothing should move by itself.
- **No network, no navigation and no pop-ups of its own:** every way out goes through `AeonCreative.click()`. No `alert`, `prompt` or `confirm`.
- **Weight:** under 1 MB on first load; compress images (WebP) and subset fonts.
- **Accessibility:** text in the creative should be real text where it can be; keep contrast at 4.5:1 for body text; links and buttons in the creative need a visible focus style.
- **Dark mode:** sites can be dark. The page around the creative may be black, so a creative with a white background should have its own edge or colour.
- **Posters:** the poster is what readers see first, under reduced motion with `"backup"`, and whenever the creative fails. It must carry the message on its own.

## Testing

1. **Simulator.**
   1. Run `node tools/serve.mjs path/to/your-package` in the kit and open `http://localhost:8090/host/?package=/package/&format=midscroll`.
   2. The simulator loads the sites' own ad script and stylesheet, prints the slot where the site would, and logs every message both ways.
   3. Try the phone window, the dark page, a resized window and reduced motion (the operating system's setting or the browser's developer tools).
2. **Checker.** `node tools/check.mjs path/to/your-package --format midscroll` applies the server's rules and messages. `node tools/pack.mjs path/to/your-package` writes the ZIP, after the same check.
3. **On the site.** Upload the package with a dry run first, create the ad as a draft, and open a preview link (see the agent brief). The preview shows the creative on a real page of the site. Nothing is counted, and a click shows where it would have gone.

### Checklist before delivery

- [ ] `check.mjs` says OK for the format, and the dry run on the site says `ok: true`.
- [ ] `ready` arrives well within 4 seconds in the simulator's log, also with a slow network (developer tools: throttling).
- [ ] The message is readable at 1280, 1440 and 1920 px wide and on a 390 px phone; for a skin, in the 160 px next to the content.
- [ ] Animation stops on `pause`, resumes on `resume`, and ends within 30 seconds.
- [ ] Reduced motion gives a still creative.
- [ ] Every exit in the manifest has a web address in the creative's `config.exits`, and each click opens the right one in the preview.
- [ ] The poster images carry the message on their own.
- [ ] Nothing loads from outside the package (developer tools: network).
