Skip to content

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:

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-Agent and decides: card for known AI agents, 204 for 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.