Cloudflare Workers, Pages & Zaraz
Add ReTarget.gg to a site behind Cloudflare: inject the widget from a Worker, drop it into a Pages project, or fire it from Zaraz. Server-trigger the Trigger Widget from a Worker.
Cloudflare is three different products that all solve this differently. This recipe covers the three paths we see most:
- Workers: inject the widget into every HTML response at the edge, using
HTMLRewriter. Zero changes to your origin. - Pages: a static or SSR project hosted on Cloudflare Pages. Drop the tag into your template.
- Zaraz: tag-manager style; paste the same one-liner into a Custom HTML tag.
If you use Cloudflare Workers for an advertiser-side backend (e.g. KYC or payment), skip to Server-triggered Trigger Widget from a Worker.
Option 1: Inject from a Worker with HTMLRewriter
Use this when your origin is yours-to-break. The Worker sits in front, gets the HTML back, and streams a rewritten response with our tag appended to <head>. Nothing on the origin changes.
export default {
async fetch(request, env) {
// Fetch the origin response, untouched.
const response = await fetch(request);
// Only rewrite HTML. Pass-through everything else (images, JSON, …).
const contentType = response.headers.get("content-type") || "";
if (!contentType.toLowerCase().includes("text/html")) {
return response;
}
return new HTMLRewriter()
.on("head", new TagInjector(env))
.transform(response);
},
};
class TagInjector {
constructor(env) {
this.env = env;
}
element(head) {
// Keep this string in sync with the tag the dashboard generates.
// The guard on data-website makes it safe to fire on every request
// (even on repeated edge passes with HTMLRewriter in dev).
const tag = `<script>
(function () {
if (document.querySelector('script[data-website="{{WEBSITE_KEY}}"]')) return;
var s = document.createElement('script');
s.src = 'https://cdn.retarget.gg/widget.js';
s.async = true;
s.setAttribute('data-pub', '{{PUBLIC_KEY}}');
s.setAttribute('data-website', '{{WEBSITE_KEY}}');
s.setAttribute('data-api', 'https://api.retarget.gg');
(document.head || document.documentElement).appendChild(s);
})();
</script>`;
head.append(tag, { html: true });
}
}Wire it up with wrangler.toml:
name = "retarget-edge"
main = "src/worker.js"
compatibility_date = "2025-09-01"
# Route it in front of your site.
[[routes]]
pattern = "yoursite.com/*"
zone_name = "yoursite.com"
Deploy:
npx wrangler deployWhy HTMLRewriter, not search-and-replace
HTMLRewriter streams the response: no buffering, no memory spike on large pages, and no risk of injecting the tag twice if your origin already contains <head> fragments in odd places. It also plays nicely with Cloudflare's streaming to the browser, so time-to-first-byte isn't affected.
Bot-only exclusions
If you want to skip the widget for known bots (SEO, monitoring, health-checks): keep the origin HTML clean for them: gate the rewrite on the User-Agent:
const BOT_RE = /(bot|crawler|spider|pingdom|uptimerobot|lighthouse|GoogleOther)/i;
export default {
async fetch(request) {
const ua = request.headers.get("user-agent") || "";
const response = await fetch(request);
if (BOT_RE.test(ua)) return response;
const ct = response.headers.get("content-type") || "";
if (!ct.toLowerCase().includes("text/html")) return response;
return new HTMLRewriter().on("head", new TagInjector()).transform(response);
},
};Option 2: Cloudflare Pages
If your site is hosted on Cloudflare Pages (static, Next.js, Astro, Nuxt, Remix, SvelteKit…), the widget is just a <script> tag in your layout/template. Same tag the dashboard shows:
<script>
(function () {
if (document.querySelector('script[data-website="{{WEBSITE_KEY}}"]')) return;
var s = document.createElement('script');
s.src = 'https://cdn.retarget.gg/widget.js';
s.async = true;
s.setAttribute('data-pub', '{{PUBLIC_KEY}}');
s.setAttribute('data-website', '{{WEBSITE_KEY}}');
s.setAttribute('data-api', 'https://api.retarget.gg');
(document.head || document.documentElement).appendChild(s);
})();
</script>For framework-specific placement (Next.js App Router, etc.), see the Next.js recipe.
Pages + Workers
Pages projects can attach a Worker as Pages Functions (functions/_middleware.js). If you want to keep the tag out of your source code, use the HTMLRewriter approach from Option 1 in a middleware function: same code, same behavior.
Option 3: Cloudflare Zaraz
Zaraz is Cloudflare's built-in tag manager. Pasting our tag is the same short story as Google Tag Manager, covered in full here: GTM recipe.
Short version for Zaraz:
Open Zaraz → Tools → Add a tool → Custom HTML
Pick Custom HTML as the tool type.
Paste the tag
Drop the exact same snippet shown in Option 2. Set the trigger to Page view (all pages).
Publish
Zaraz deploys the tag to your zone immediately: no Save → Publish cycle like GTM.
Server-triggered Trigger Widget from a Worker
If you're running advertiser-side logic in a Worker (for example, a KYC check that denies a user), you can open the Trigger Widget overlay on the user's page without calling back through your origin.
The two endpoints you need:
export default {
async fetch(request, env) {
// …your KYC / risk check…
const declined = await runKyc(request);
if (!declined) return new Response("approved", { status: 200 });
// 1. Fire the trigger from the Worker. The browser is already polling
// /v1/widget-decline/session/{sessionId}: the next poll will pick
// it up and render the overlay.
await fetch("https://api.retarget.gg/v1/widget-decline/trigger", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${env.RETARGET_INTEGRATION_SECRET}`,
},
body: JSON.stringify({
sessionId: request.headers.get("x-retarget-session"),
reason: "kyc_failed",
}),
});
return new Response("declined", { status: 200 });
},
};The client-side widget-decline.js emits the session ID when it mounts. Capture it and forward with your Worker request. Full client-side setup: Trigger Widget integration.
Integration secret: not the public key
/v1/widget-decline/trigger requires a server-only integration secret (bearer token). Never ship it to the browser. Put it in wrangler secret put RETARGET_INTEGRATION_SECRET and access it via env.RETARGET_INTEGRATION_SECRET.
Cache considerations
If your Pages project or Worker sits behind Cloudflare's cache:
- The widget is a static JS file served from
https://cdn.retarget.gg/widget.jswith a shortCache-Control. You don't need to cache it yourself. - The API (
api.retarget.gg) responses setCache-Control: no-store: decisions are per-request. Don't addCache-Rulesthat cache them. - Your origin HTML caches as normal: the
HTMLRewriterinjection from Option 1 re-runs on every HTML response, but sincewidget.jsitself is a tiny external script, there's no duplicate-content concern.
Verify
Visit your site with DevTools open
Check the Network tab for
widget.jsand aGET /v1/decisioncall toapi.retarget.gg. Both should be 200.Check a blocked country
On
localhostadddata-dev-country="US"to the injected script tag to simulate a blocked visitor (see Debugging). On production, spin up a quick VPN test.Confirm events in the dashboard
Open Websites → your site → Analytics. A successful page-load produces a
snippet-ping; a blocked user produces animpressionper card. If nothing shows after 2 minutes, revisit Debugging.
Related
Publisher
Geo Popup & Decline Popup overview
The product framing for the install you just read.
Advertiser
CPC, CPA, eCPM in one auction
The demand side of the network — useful context even for publishers.
Essays
Long-form strategy
How others think about geo-blocking, KYC declines, and bid models.
Need help with setup?
Send us your website stack, target regions, and whether you are installing Geo Popup or Decline Popup.