Skip to content

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:

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.