# aon.js Web Plugin

Source: https://docs.aon.pro/api/web-plugin

> Derived from the same AON Docs release as the source page.

`aon.js` is the script that puts a Content Assistant placement on a web page. Add it once per page and it reads the placement's styles from AON, draws them, opens the chat panel, and attributes everything to the placement. This page is the reference for the tag, the DOM it expects and creates, the requests it makes, and the `window.AON` API. For the step-by-step setup, read the [Content Assistant guide](https://docs.aon.pro/guides/content-assistant) first.

## The tag

```html
<script
  src="https://plugin.aon.pro/aon.js"
  data-placement-id="plc_your_placement_id"
  data-lang="en"
  referrerpolicy="origin"
  async
></script>
```

Put it once on every page, before `</body>`, normally in the shared layout. A second copy on the same page does nothing.

| Attribute | Required | Value |
| --- | --- | --- |
| `src` | yes | `https://plugin.aon.pro/aon.js` |
| `data-placement-id` | yes | The Content Assistant placement id from the Developer Portal, `[A-Za-z0-9_-]`, 1–64 characters. Also the attribution key: offers shown by this assistant are credited to the placement's application. |
| `data-lang` | no | Language of the panel's own text (header, buttons, prompts). BCP 47 tag, matched case-insensitively. See [Panel languages](https://docs.aon.pro/api/web-plugin#panel-languages). Default `en`. It never follows the visitor's browser. |
| `referrerpolicy` | yes | `origin`. Keeps the full page URL (path and query) out of the script request when the page sends a permissive `Referrer-Policy`. |
| `async` | recommended | The script never blocks rendering; it does all its work after load. |
| `data-prewarm` | no | `off` (default), `idle` or `eager`. Whether to build the hidden chat panel before the first click. `idle` and `eager` download the panel (about 40 KB gzipped) on every visit unless the visitor has asked for Save-Data; use them on pages with a high open rate. |
| `data-publisher-id` | no | An identifier of your choice. It only namespaces the device id stored on your origin (see [Requests](https://docs.aon.pro/api/web-plugin#requests)) and is never sent to AON. |
| `data-debug` | no | Any value except `false` turns on the panel's diagnostic logging. |

`data-prewarm` values are case-sensitive. An invalid `data-placement-id` or `data-prewarm` is ignored with one `console.debug` line and never replaces an earlier valid value.

If `document.currentScript` is not available to the script (bundled, loaded as a module, or inlined by a tag manager), the attributes cannot be read. Then load it with a classic non-`async` tag and call `AON.init({ placementId: "…" })` synchronously right after it. The placement id is fixed when the document finishes parsing (`DOMContentLoaded`), or earlier if the panel is opened before that; a later `init` only logs a debug line.

## What the script draws

The placement's styles are switched on and off in the Developer Portal. The script fetches that setup on load and, once the document has parsed, draws what is on:

| Style | Drawn as | Needs in your HTML |
| --- | --- | --- |
| Floating button | A fixed button in the viewport corner (`button[data-aon-launcher]`, `aria-label="Open assistant"`), with an optional prompt bubble when "Show a prompt message" is on. A click opens the panel with the first suggested question for the page, or an empty panel when AON has not read the page. | Nothing. |
| End of content | Up to five suggested questions, plus a question box when "Include a question box" is on, rendered inside your spot as `section[aria-label="Assistant suggestions"]`. | One `<div data-aon-slot="end-of-content"></div>` on every page, right after its main content. The script looks for it once, when the document has parsed and the placement setup has arrived, and does not watch for later insertions; put it in the server-rendered or static HTML so it is there in time. |
| Inline mention, Inline card | Not drawn by the web script yet. Switching them on has no effect on web. | Nothing. |

The spot renders nothing until AON has read the page (see [Requests](https://docs.aon.pro/api/web-plugin#requests)). Leave the `div` empty with no fixed height; the script appends its own nodes and removes them again when the page's reading is no longer valid. One spot per page: only the first is used.

On every page the script also adds a page-level `div[data-aon-chat]` shell that holds the chat panel and a one-line disclosure, "Sponsored results by agent offer network (AON)", linking to aon.pro with `rel="sponsored nofollow noopener"`. The panel itself is a cross-origin `<iframe title="AON">` from `https://plugin.aon.pro`, sandboxed (`allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox`, no top navigation) with `allow="web-share; clipboard-write"` and `referrerpolicy="origin"`. Everything the assistant says, every offer and every price lives inside that iframe; the host page never receives offer data.

## Page requirements

-   **HTTPS.** On an `http://` page the script still loads and the floating button still opens the panel, but AON never reads the page, so the End of content spot stays empty and the panel opens without suggestions.
-   **Own domain.** AON reads a page by fetching its public URL. Pages behind a login or on shared preview domains (`*.vercel.app`, `*.netlify.app`, `*.pages.dev`, `*.github.io`, tunnels) are not read.
-   **Single-page apps.** AON reads a page on a full load. After a client-side route change (`pushState`, `replaceState`, `popstate`) the script hides the suggestions it drew for the previous URL and does not read the new one until the next full load (or until `init({ lang })` changes the locale, which reads the current URL). The chat panel keeps working.

### Content-Security-Policy

If the page sends one, allow:

```text
script-src  https://plugin.aon.pro
frame-src   https://plugin.aon.pro
connect-src https://plugin.aon.pro https://api.aon.pro
```

No `style-src` change is needed: styles are set property by property, never through injected `<style>` elements or `style` attributes.

## Requests

The script makes three kinds of requests. None carries page text, query strings, hashes or anything read from the DOM.

| Request | When | Sends | Notes |
| --- | --- | --- | --- |
| `GET https://api.aon.pro/v1/public/placements/{placement_id}/setup` | Once per page load, after the placement id is fixed. No credentials. | The placement id in the path | Returns `type`, `status` and `content_assistant_setup` (`styles`, `with_prompt`, `with_ask_box`). A valid response is stored in `localStorage` as `aon:setup:<placement_id>`. With a cached copy, the cached setup controls the current load and the fresh response only updates the next one, so a portal change shows on the second load. `404`, a status other than `live` or a type other than `content_assistant` means there is no setup: an uncached load draws nothing, and the cache is cleared so the next load draws nothing either. |
| `POST https://plugin.aon.pro/api/page-contexts/resolve` | On HTTPS pages: on load, again if `init({ lang })` changes the locale or the page is restored from the back-forward cache; polled while the answer is `PENDING`. No credentials. | `submitted_url` = `origin + pathname`; `semantic_context.locale` when the language tag is valid | This is how AON reads the page. `READY` carries one to five suggested questions and the prompt text; anything else, including network errors and `403`/`429`/`503`, degrades to "no suggestions" without touching the host page. Never sent on HTTP pages. |
| `https://plugin.aon.pro/panel/…` | When the panel is first opened, or earlier with `data-prewarm` | The placement id (`aon_pid`) on the iframe URL; language, theme and your input go to the panel by `postMessage` | An ordinary cross-origin document, so it may carry `plugin.aon.pro` cookies. It talks to its own backend; every offer query it makes carries `X-AON-Placement-Id` with your placement id. |

Two `localStorage` keys may be written on your origin: `aon:setup:<placement_id>` (above) and `aon:did:<publisher_id>` (a random, persistent device identity created the first time the panel connects). Neither contains data from your page.

## JavaScript API

`window.AON` has exactly four keys: `version`, `init`, `placement` and `chat`. `version`, `placement` and `chat` are read-only, and `placement` and `chat` are frozen objects. Methods never throw: `init` and `placement` ignore invalid values, `chat.open` with non-object options behaves as `open()` and warns, and invalid `input` fields come back as `invalid` in the `result` event.

### `AON.version`

The script's version string.

### `AON.init(config)`

Publisher-level configuration. Every field is optional and a missing field keeps its current value; the tag's `data-*` attributes go through the same call before `window.AON` exists.

| Field | Type | Effect |
| --- | --- | --- |
| `placementId` | `string` | Same as `data-placement-id`. Honoured only before the id is fixed (see [The tag](https://docs.aon.pro/api/web-plugin#the-tag)). |
| `lang` | `string` | Same as `data-lang`. A new value re-reads the page for that locale and applies to the next panel session; an open panel keeps its language. |
| `prewarm` | `"off" | "idle" | "eager"` | Same as `data-prewarm`. |
| `publisherId` | `string` | Same as `data-publisher-id`. Takes effect only before the panel first connects. |
| `debug` | `boolean` | Same as `data-debug`. |

### `AON.placement`

Manual control of the two web styles. These calls always work, whatever the portal switches say, and they take precedence: once you have called one of them on a page, the script no longer draws that style automatically there. Each returns a function that removes what the call drew and nothing else.

```js
// Floating button: "icon" alone, or "prompt" = icon plus the suggestion bubble.
const removeButton = AON.placement.floating("prompt");
 
// End of content, into a container you own. "ask" = questions + text box, "buttons" = questions only.
const removeSpot = AON.placement.endOfContent({
  container: document.querySelector('[data-aon-slot="end-of-content"]'),
  variant: "ask",
});
```

`floating` keeps at most one button on the page; a second valid call replaces the first. `endOfContent` draws only when AON has read the page and clears itself when that reading stops being valid.

### `AON.chat`

Direct control of the chat panel, for sites that build their own entry points. The placement styles use these same methods.

| Method | Returns | Behaviour |
| --- | --- | --- |
| `open(options?)` | input id or `undefined` | Shows the panel. With `options.input` the message is submitted in the same call. `options.presentation` sets `lang` and a `theme` (`accent`, `radius`) for this open. `options.awaitInput: true` opens a "preparing" state that waits for a following `push`. |
| `push(input)` | input id | Submits host input without changing visibility. Before the first `open` it is buffered and sent when the panel opens. In a visible conversation it appears as a tappable suggestion rather than being sent. In a panel hidden by `close()` it is dropped (`result` with reason `hidden`). |
| `close()` | — | Hides the panel and keeps the conversation. Submissions still in flight are dropped without a `result`. |
| `destroy()` | — | Removes the iframe and the conversation, and clears anything buffered before the first `open`. Event subscriptions survive; a later `open` starts fresh. The `div[data-aon-chat]` shell stays. |
| `isOpen()` | `boolean` | Whether the panel is visible right now. |
| `on(event, handler)` | unsubscribe function | Events: `opened`, `closed` (`{ reason: "host-close" | "visitor-close" | "destroy" }`), `result` (`{ inputId, results: [{ field, outcome, reason? }] }`, at most one per submitted input). Handlers run asynchronously; one throwing handler does not affect the others. |

`input` is a plain object:

| Field | Type | Meaning |
| --- | --- | --- |
| `message` | `string` | The visitor message to submit, trimmed, up to 200 Unicode code points. This is the only field the assistant reads. |
| `refs.page` | `{ url } | null` | Override the page URL AON reads. The script already uses `origin + pathname`; set this only for hash or query routing, a canonical URL, or `null` to opt a page out. |
| `refs.category` | `{ id } | null` | A canonical AON taxonomy id to scope offers. Accepted but not applied yet (`outcome: "dropped"`). |
| `refs.offer` | `{ id } | null` | Reserved (`outcome: "unsupported"`). |
| `attribution` | `{ placementId?, trigger? }` | Analytics labels only (`trigger`: `click`, `auto`, `api`). Offer attribution comes from the tag's placement id, not from here. |

```js
const inputId = AON.chat.open({
  input: { message: "Which of these laptops is best for video editing?" },
});
AON.chat.on("result", ({ inputId: id, results }) => {
  if (id === inputId) console.log(results);
});
```

## Panel languages

`data-lang`, `init({ lang })` and `presentation.lang` accept the same set, case-insensitively. For Chinese, a `Hant` script subtag or a `TW`, `HK` or `MO` region selects Traditional; anything else is Simplified. Other tags are matched on their primary subtag (`pt-BR` → `pt`); an unknown tag falls back to English. The assistant's answers follow the language the visitor writes in, not this setting.

| Tag | Language |
| --- | --- |
| `en` | English |
| `zh-CN`, `zh-Hans`, `zh` | Simplified Chinese |
| `zh-TW`, `zh-Hant` | Traditional Chinese |
| `ja` | Japanese |
| `ko` | Korean |
| `id` | Indonesian |
| `ms` | Malay |
| `th` | Thai |
| `vi` | Vietnamese |
| `fil`, `tl` | Filipino |
| `es` | Spanish |
| `pt`, `pt-PT` | Portuguese |
| `de` | German |
| `fr` | French |
| `hi` | Hindi |
| `bn` | Bengali |
| `ru` | Russian |
| `ar` | Arabic |
| `tr` | Turkish |

## Attribution

The placement id on the tag is the only attribution you need. It travels with the panel to the chat backend as `aon_pid`, which adds `X-AON-Placement-Id` to every offer query it makes, so impressions, clicks and conversions land on the placement's application in the Developer Portal. Conversions arrive on the application's [conversion Webhook](https://docs.aon.pro/api/webhooks).

## Verify an integration

1.  `curl -s https://your-site/page | grep -c 'data-placement-id="plc_…"'` returns `1`, and the same for `data-aon-slot="end-of-content"`.
2.  `curl -s https://api.aon.pro/v1/public/placements/plc_…/setup` returns `"status": "live"` and a non-empty `styles` list.
3.  On the live page, the floating button appears and opens an `<iframe title="AON">` whose `src` contains `aon_pid=plc_…`. The End of content spot fills with questions once AON has read the page; a page AON has never seen can take a while on its first visit.
4.  For details, open the browser console with the Verbose level: loader messages start with `[AON]`.

The Developer Portal's **Copy for LLM** button on the placement page gives a coding agent this integration as a runbook, including a Playwright script that performs the checks above.
