Integration
A publisher's job in ABC is small: take the card 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 + an agent-UA gate |
| Cloudflare | Worker | ~30-line worker, one deploy |
| AWS CloudFront | Lambda@Edge | ~40-line function |
| No CDN control / SPA | Browser JS | ~10 lines (fallback) |
Most publisher CDNs are either ESI-capable (Akamai, Fastly, self-hosted Varnish) or support a small edge function (Cloudflare, CloudFront) — so the two main paths, an ESI tag and a small edge worker, cover the large majority of stacks.
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 card, 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.
Enable ESI processing on your text/html responses, gated on the User-Agent
so the include resolves ONLY for AI agents — a human request is never
enriched and never calls the provider. Match against the published agent
list (schema/agents.json):
Akamai — Property Manager → match on User-Agent (agent list) → behavior
"Edge Side Includes" → Enable
Fastly — adapters/fastly.vcl (gates beresp.do_esi on an agent-UA match)
Varnish — adapters/varnish.vcl (gates beresp.do_esi on an agent-UA match)
-->
<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;
Gate ESI on the User-Agent so the <esi:include> resolves only for AI agents
(see Agents for the list) — a human request never triggers a card
request. The response is then cacheable by URL with no Vary: User-Agent.
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 card,
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:
// classify the request at the edge and, for AI agents only, fetch the brand
// card from your provider and inline it. Humans get the page unchanged
// and never trigger a card request.
//
// 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
// Agent markers — keep in sync with schema/agents.json (word-boundary, case-insensitive).
const AGENT_UA =
/\b(GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-User|Claude-SearchBot|Google-CloudVertexBot|PerplexityBot|Perplexity-User|CCBot|Meta-ExternalAgent|meta-externalfetcher|Bytespider|YouBot|Diffbot|MistralAI-User|Amazonbot)\b/i;
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;
// Classify at the edge: only AI agents get a card request.
const ua = request.headers.get("User-Agent") || "";
if (!AGENT_UA.test(ua)) return res;
// Build the card request: provider URL + this page. The UA is
// forwarded for the provider's reporting only (it does not affect the card).
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": ua },
});
// 200 = a card for this page; 204 = no eligible brand (no-fill).
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 Worker classifies the request from the
User-Agent (the agent list) and fetches the card only for AI
agents — human traffic is passed straight through.
CloudFront (Lambda@Edge)
Same idea as an origin-response Lambda: fetch the card, 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 classifies the request at the edge and, for AI
// agents only, fetches the brand card from your provider and inlines it.
// Humans get the page unchanged and never trigger a card request.
//
// 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.
"use strict";
const https = require("https");
const FRAGMENT_ENDPOINT = "https://provider.example/fragment"; // your provider URL
// Agent markers — keep in sync with schema/agents.json (word-boundary, case-insensitive).
const AGENT_UA =
/\b(GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-User|Claude-SearchBot|Google-CloudVertexBot|PerplexityBot|Perplexity-User|CCBot|Meta-ExternalAgent|meta-externalfetcher|Bytespider|YouBot|Diffbot|MistralAI-User|Amazonbot)\b/i;
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 = no eligible brand (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 || "";
// Classify at the edge: only AI agents get a card request. The UA is then
// forwarded for the provider's reporting only (it does not affect the card).
if (!AGENT_UA.test(ua)) return response;
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 classifies the client from navigator.userAgent and, for AI agents only,
// fetches the card (JSON form) and appends it — so a human browser never
// fetches or shows a card.
//
// 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.
// Agent markers — keep in sync with schema/agents.json (word-boundary, case-insensitive).
const AGENT_UA =
/\b(GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-User|Claude-SearchBot|Google-CloudVertexBot|PerplexityBot|Perplexity-User|CCBot|Meta-ExternalAgent|meta-externalfetcher|Bytespider|YouBot|Diffbot|MistralAI-User|Amazonbot)\b/i;
(async () => {
if (!AGENT_UA.test(navigator.userAgent || "")) return; // humans: no fetch, no card
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 */
}
})();
Classify at your edge
Classification happens once, at your edge, before the cache: match the request's
User-Agent against the published agent list (schema/agents.json —
word-boundary, case-insensitive). Only a match triggers a card request; everyone
else gets the page unchanged. Keeping the decision at the edge means the response is
cacheable by URL — the same card is reused across agents — and a human request never
reaches the provider.