ReTarget.gg
Guides & recipes

Integrate ReTarget.gg with Next.js

Add the ReTarget.gg script to a Next.js app correctly, including script loading strategy and common pitfalls.

3 min readReviewed Jul 15, 2026

Production install for Next.js 14/15 App Router. Covers env-scoped keys, CSP headers, and a typed helper for the Trigger Widget flow.

Public key + website key required

Pages Router?

The patterns translate 1:1. Swap app/layout.tsx for pages/_app.tsx and wrap your component tree with the same <Script> placement.

1. Environment variables

Put keys in .env.local (or your Vercel project env):

# Public: safe in the browser (per-website keys)
NEXT_PUBLIC_RETARGET_PUB=pk_your_public_key
NEXT_PUBLIC_RETARGET_WEBSITE=web_your_website_key
 
# Server only: Trigger Widget flow
RETARGET_INTEGRATION_SECRET=its_your_integration_secret
RETARGET_API_URL=https://api-v2.retarget.gg

Vercel users: set these under Project Settings → Environment Variables with the right env (production / preview / development).

2. Install widget.js in app/layout.tsx

// app/layout.tsx
import Script from "next/script";
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          id="retarget"
          src="https://cdn.retarget.gg/widget.js"
          data-pub={process.env.NEXT_PUBLIC_RETARGET_PUB}
          data-website={process.env.NEXT_PUBLIC_RETARGET_WEBSITE}
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

strategy="afterInteractive" loads the script after hydration: it never blocks first paint. Next.js server-renders the tag with the env values baked in, so no client-side round trip.

3. Section widget on a specific page

// app/blog/[slug]/page.tsx
import Script from "next/script";
 
export default function BlogPost() {
  return (
    <article className="prose">
      {/* ...your content... */}
 
      <div id="retarget-ads" className="my-10" />
 
      <Script
        id="retarget-section"
        src="https://cdn.retarget.gg/widget-section.js"
        data-pub={process.env.NEXT_PUBLIC_RETARGET_PUB}
        data-website={process.env.NEXT_PUBLIC_RETARGET_WEBSITE}
        data-container="retarget-ads"
        strategy="afterInteractive"
      />
    </article>
  );
}

4. CSP-friendly setup

If you use a Content Security Policy, allow ReTarget.gg's origins in your next.config.ts or middleware:

// next.config.ts
const cspHeader = `
  default-src 'self';
  script-src 'self' 'unsafe-inline' https://retarget.gg;
  connect-src 'self' https://api-v2.retarget.gg https://retarget.gg;
  frame-src https://retarget.gg;
  img-src 'self' data: https: blob:;
  style-src 'self' 'unsafe-inline';
`.replace(/\s{2,}/g, " ").trim();
 
const nextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [{ key: "Content-Security-Policy", value: cspHeader }],
      },
    ];
  },
};
 
export default nextConfig;

Strict CSP ('nonce-…')

If you use nonce-based CSP, pass the nonce to <Script nonce={nonce} /> so Next.js forwards it to the injected <script>. Otherwise the browser rejects it.

5. TypeScript for the Trigger Widget session event

The Trigger Widget is the only surface that dispatches a DOM event: retarget:decline-session. Augment the window type so your event handlers are typed:

// types/retarget.d.ts
interface RetargetDeclineSessionDetail {
  sessionId: string;
}
 
declare global {
  interface WindowEventMap {
    "retarget:decline-session": CustomEvent<RetargetDeclineSessionDetail>;
  }
 
  interface Window {
    __RETARGET_DECLINE_SESSION_ID?: string;
  }
}
 
export {};

No events from geo-blocking / section widgets

The geo-blocking (widget.js) and section (widget-section.js) widgets do not emit DOM events today. If you need to forward decision/impression/click activity into your own analytics, use the Scheduled CSV exports in the dashboard (see Analytics & reporting): the CSV arrives on your cadence with the same rows the widget emits.

6. Trigger Widget flow: full example

Server route for triggering

// app/api/retarget/trigger/route.ts
import { NextResponse } from "next/server";
 
const API = process.env.RETARGET_API_URL ?? "https://api-v2.retarget.gg";
 
export async function POST(req: Request) {
  const secret = process.env.RETARGET_INTEGRATION_SECRET;
  if (!secret) {
    return NextResponse.json(
      { error: "RETARGET_INTEGRATION_SECRET missing" },
      { status: 500 },
    );
  }
 
  const { sessionId } = (await req.json().catch(() => ({}))) as {
    sessionId?: string;
  };
  if (!sessionId) {
    return NextResponse.json({ error: "sessionId required" }, { status: 400 });
  }
 
  // Pair sessionId with your authenticated user here before triggering
  // e.g. lookup the user session and persist the pairing.
 
  const res = await fetch(`${API}/v1/widget-decline/trigger`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${secret}`,
    },
    body: JSON.stringify({ sessionId }),
  });
 
  if (!res.ok) {
    return NextResponse.json(
      { error: await res.text() },
      { status: res.status },
    );
  }
  // The API returns { ok: true } on success
  const body = await res.json();
  return NextResponse.json(body, { status: 200 });
}

Client bridge

// app/declined/decline-bridge.tsx
"use client";
import { useEffect } from "react";
 
export function DeclineBridge() {
  useEffect(() => {
    const send = (sessionId: string) =>
      fetch("/api/retarget/trigger", {
        method: "POST",
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ sessionId }),
      });
 
    const handler = (ev: WindowEventMap["retarget:decline-session"]) => {
      void send(ev.detail.sessionId);
    };
 
    window.addEventListener("retarget:decline-session", handler);
    const existing = window.__RETARGET_DECLINE_SESSION_ID;
    if (existing) void send(existing);
 
    return () =>
      window.removeEventListener("retarget:decline-session", handler);
  }, []);
 
  return null;
}

The decline page

// app/declined/page.tsx
import Script from "next/script";
import { DeclineBridge } from "./decline-bridge";
 
export default function DeclinedPage() {
  return (
    <>
      <Script
        id="retarget-decline"
        src="https://cdn.retarget.gg/widget-decline.js"
        strategy="beforeInteractive"
        data-pub={process.env.NEXT_PUBLIC_RETARGET_PUB}
        data-website={process.env.NEXT_PUBLIC_RETARGET_WEBSITE}
      />
      <DeclineBridge />
      <main className="prose mx-auto py-20">
        <h1>We can&apos;t proceed with this account</h1>
        <p>You may see alternative offers while we review.</p>
      </main>
    </>
  );
}

Server-only secrets

RETARGET_INTEGRATION_SECRET must never have NEXT_PUBLIC_: if it does, Next will ship it to the browser and you'll need to rotate in the dashboard immediately.

7. Preview / staging hosts

Each ReTarget.gg website is tied to a single registered domain. For Vercel preview URLs you have two paths:

  • Treat staging as its own website. Add a second website in the dashboard with your-project.vercel.app as the domain, use its keys on preview deploys, and keep production keys isolated.

  • Point data-api at a staging API if you have one, so preview traffic doesn't pollute production analytics:

    <Script
      src="https://cdn.retarget.gg/widget.js"
      data-pub={process.env.NEXT_PUBLIC_RETARGET_PUB}
      data-website={process.env.NEXT_PUBLIC_RETARGET_WEBSITE}
      data-api={process.env.NEXT_PUBLIC_RETARGET_API_URL}
      strategy="afterInteractive"
    />

Gotchas

  • Middleware redirects: If your app redirects based on geo at the edge, make sure ReTarget.gg's script tag still ends up in the final rendered HTML. A middleware that rewrites to a blank page won't run the widget.
  • Partial prerendering (Next 15): <Script> works in PPR; keep it out of suspense boundaries or it'll render twice.
  • Static exports (output: "export"): Works fine. The script is fully client-side, no server runtime needed.

Need help with setup?

Send us your website stack, target regions, and whether you are installing Geo Popup or Decline Popup.

Integrate ReTarget.gg with Next.js

Add the ReTarget.gg script to a Next.js app correctly, including script loading strategy and common pitfalls.

Next.js Setup and Installation Guide for ReTarget.gg