ReTarget.gg
Guides & recipes

Next.js recipe

Production-grade ReTarget.gg install for Next.js App Router: env-scoped keys, CSP-friendly script loading, TypeScript-safe Trigger Widget flow.

3 min readReviewed Apr 27, 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.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.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.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.

Next.js recipe | Docs | ReTarget.gg