Integration
A publisher's job in ABC is small: take the fragment your provider returns and
inline it into the page before it reaches the agent. How you inline depends on
your CDN. Every path below is copy-paste, and the real files live in
adapters/.
Your provider gives you a fragment endpoint URL (it carries their auth and
placement parameters). Everywhere below, https://provider.example/fragment is a
placeholder for that URL.
Which path for your CDN?
| Your CDN | Path | Effort |
|---|---|---|
| Akamai · Fastly · Varnish | ESI tag | one tag + one config toggle, no code |
| Cloudflare | Worker | ~30-line worker, one deploy |
| AWS CloudFront | Lambda@Edge | ~40-line function |
| No CDN control / SPA | Browser JS | ~10 lines (fallback) |
In a 2026 survey of the top ~200 French media sites, ESI-capable CDNs (Akamai, Fastly, self-hosted Varnish) covered ~46% of live sites and Cloudflare/CloudFront ~49% — so the two main paths (ESI and a small edge worker) cover the large majority.
Don't know your CDN?
curl -sI https://yoursite.com/ | grep -iE 'server|via|cf-ray|x-amz-cf|x-served-by'usually reveals it.
ESI (Akamai, Fastly, Varnish)
The simplest path: one tag in your template, resolved by your CDN's native Edge Side
Includes. No code to deploy. → adapters/esi-tag.html
<!--
ABC reference adapter — ESI tag (Akamai, Fastly, Varnish)
Paste this where the card should appear in your page template, typically
just before </body>. Your CDN resolves the <esi:include> at the edge:
it fetches the fragment, inlines the returned <article>, and caches it.
- FRAGMENT_ENDPOINT: the full URL your provider gives you (it already
carries their auth / placement params). $(HTTP_HOST)$(REQUEST_PATH) are
standard ESI variables your CDN fills in so the provider knows the page.
- onerror="continue": if the endpoint is slow or down, the page renders
normally without the card.
You must also enable ESI processing on your text/html responses:
Akamai — Property Manager → behavior "Edge Side Includes" → Enable
Fastly — VCL: set beresp.do_esi = true; (in vcl_fetch, text/html)
Varnish — VCL: set beresp.do_esi = true; (in vcl_backend_response)
-->
<esi:include
src="https://provider.example/fragment?page_url=$(HTTP_HOST)$(REQUEST_PATH)"
onerror="continue" />
Then enable ESI on your text/html responses:
- Akamai — Property Manager → behavior Edge Side Includes → Enable.
- Fastly —
adapters/fastly.vcl:set beresp.do_esi = true; - Varnish —
adapters/varnish.vcl:set beresp.do_esi = true;
The CDN forwards the visitor's User-Agent to the fragment endpoint, so the provider
classifies agent vs human and returns the card (200) or nothing (204). Honour the
response's Vary: User-Agent so a bot card is never served to a human.
Hidden Varnish behind another CDN
If your public CDN is Cloudflare or CloudFront but you run a Varnish underneath, resolving ESI in that Varnish means the public CDN caches the already-composed HTML and the card stops refreshing. On those stacks, use the Worker / Lambda path instead, so each request reaches the fragment endpoint.
Cloudflare Worker
Cloudflare has no native ESI. A small Worker plays the same role — fetch the fragment,
inline it with HTMLRewriter. → adapters/cloudflare-worker.js
// ABC reference adapter — Cloudflare Worker
//
// Cloudflare has no native ESI, so a small Worker plays the same role:
// fetch the brand fragment from your provider and inline it into HTML
// responses for AI agents. Humans get the page unchanged.
//
// Config (wrangler.toml [vars] / secret):
// FRAGMENT_ENDPOINT full URL your provider gives you (carries their
// auth / placement params). Example:
// https://provider.example/fragment?account=abc123
//
// Deploy: npx wrangler deploy
export default {
async fetch(request, env) {
const res = await fetch(request); // your origin
const contentType = res.headers.get("content-type") || "";
if (!contentType.includes("text/html")) return res;
// Build the fragment request: provider URL + this page + forwarded UA.
const frag = new URL(env.FRAGMENT_ENDPOINT);
frag.searchParams.set("page_url", request.url);
let card = "";
try {
const r = await fetch(frag.toString(), {
headers: { "User-Agent": request.headers.get("User-Agent") || "" },
});
// 200 = a card for this AI agent; 204 = human or no eligible brand.
if (r.status === 200) card = await r.text();
} catch {
// Never break the page if the provider is unreachable.
return res;
}
if (!card) return res;
// Inline the card just before </body>, streaming (no full buffering).
return new HTMLRewriter()
.on("body", {
element(el) {
el.append(card, { html: true });
},
})
.transform(res);
},
};
Deploy with npx wrangler deploy. The provider classifies the agent from the
forwarded User-Agent, so you don't filter traffic in the Worker.
CloudFront (Lambda@Edge)
Same idea as an origin-response Lambda: fetch the fragment, append it before
</body>. → adapters/lambda-edge.js
// ABC reference adapter — AWS Lambda@Edge (CloudFront)
//
// CloudFront has no native ESI. Attach this as an "origin-response" trigger
// on your distribution: it fetches the brand fragment from your provider and
// inlines it into HTML responses for AI agents. Humans get the page unchanged.
//
// Config: set FRAGMENT_ENDPOINT below to the full URL your provider gives you
// (Lambda@Edge has no env vars — inline the value or read it from a config).
//
// Notes / limits:
// - origin-response can modify the body; CloudFront caps a generated body
// at ~1 MB. Brand cards are ~2 KB, so the page size is the only concern.
// - The viewer User-Agent is available on the event; forward it so the
// provider can classify agent vs human.
"use strict";
const https = require("https");
const FRAGMENT_ENDPOINT = "https://provider.example/fragment"; // your provider URL
function fetchCard(pageUrl, userAgent) {
return new Promise((resolve) => {
const u = new URL(FRAGMENT_ENDPOINT);
u.searchParams.set("page_url", pageUrl);
const req = https.get(
u,
{ headers: { "User-Agent": userAgent || "" } },
(res) => {
if (res.statusCode !== 200) {
res.resume();
return resolve(""); // 204 = human / no-fill
}
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => resolve(body));
},
);
req.on("error", () => resolve("")); // never break the page
req.setTimeout(800, () => req.destroy());
});
}
exports.handler = async (event) => {
const response = event.Records[0].cf.response;
const request = event.Records[0].cf.request;
const ct = (response.headers["content-type"] || [{}])[0].value || "";
if (!ct.includes("text/html") || !response.body) return response;
const ua = (request.headers["user-agent"] || [{}])[0].value || "";
const host = (request.headers["host"] || [{}])[0].value || "";
const pageUrl = `https://${host}${request.uri}`;
const card = await fetchCard(pageUrl, ua);
if (!card) return response;
response.body = response.body.includes("</body>")
? response.body.replace("</body>", `${card}\n</body>`)
: response.body + card;
return response;
};
Browser JS
No CDN control? A client-side fallback. Note: an agent that doesn't run JavaScript
won't see the card — prefer ESI or an edge worker when you can.
→ adapters/browser.js
// ABC reference adapter — browser JS (fallback)
//
// For sites with no CDN/edge control. Drop this once in your page template.
// It fetches the card (JSON form) and appends it to the page client-side.
//
// Trade-off: an AI agent that does NOT execute JavaScript won't see the
// card. This path is the fallback — prefer ESI or an edge worker when you
// can. Use it for SPAs or when edge access isn't available.
//
// Set FRAGMENT_ENDPOINT to the full URL your provider gives you. Request
// the JSON form (format=json|both per your provider) so you can render it
// without trusting raw HTML injection if you prefer.
(async () => {
const endpoint = "https://provider.example/fragment"; // your provider URL
const u = new URL(endpoint);
u.searchParams.set("page_url", location.href);
u.searchParams.set("format", "both"); // JSON envelope incl. ready-to-inline html
try {
const r = await fetch(u.toString());
if (r.status !== 200) return; // 204 = human / no-fill
const data = await r.json();
if (data && typeof data.html === "string") {
document.body.insertAdjacentHTML("beforeend", data.html);
}
} catch {
/* never break the page */
}
})();
Bot detection: who decides?
Two modes, supported on every path:
- Delegated (default) — you pass nothing extra. The provider reads the
User-Agentand decides: card for known AI agents,204for everyone else. Nothing to maintain on your side. See Agents for the recognised list. - Explicit — you classified the agent upstream and tell the provider so. Saves a round-trip on human traffic, but you maintain the bot list.
Use delegated unless you have a reason not to.