Next.js recipe
Production-grade AffilFinder install for Next.js App Router: env-scoped keys, CSP-friendly script loading, TypeScript-safe decline flow.
On this page
Production install for Next.js 14/15 App Router. Covers env-scoped keys, CSP headers, and a typed helper for the decline flow.
YOUR_PUBLIC_KEY placeholders.Sign in to pre-fill with your real keys →1. Environment variables
Put keys in .env.local (or your Vercel project env):
# Public — safe in the browser (per-website keys)
NEXT_PUBLIC_AFFILFINDER_PUB=pk_your_public_key
NEXT_PUBLIC_AFFILFINDER_WEBSITE=web_your_website_key
# Server only — decline flow
AFFILFINDER_INTEGRATION_SECRET=its_your_integration_secret
AFFILFINDER_API_URL=https://api.affilfinder.comVercel 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="affilfinder"
src="https://cdn.affilfinder.com/widget.js"
data-pub={process.env.NEXT_PUBLIC_AFFILFINDER_PUB}
data-website={process.env.NEXT_PUBLIC_AFFILFINDER_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="affilfinder-ads" className="my-10" />
<Script
id="affilfinder-section"
src="https://cdn.affilfinder.com/widget-section.js"
data-pub={process.env.NEXT_PUBLIC_AFFILFINDER_PUB}
data-website={process.env.NEXT_PUBLIC_AFFILFINDER_WEBSITE}
data-container="affilfinder-ads"
strategy="afterInteractive"
/>
</article>
);
}4. CSP-friendly setup
If you use a Content Security Policy, allow AffilFinder's origins in your next.config.ts or middleware:
// next.config.ts
const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-inline' https://affilfinder.com;
connect-src 'self' https://api.affilfinder.com https://affilfinder.com;
frame-src https://affilfinder.com;
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;5. TypeScript for the decline session event
The decline widget is the only surface that dispatches a DOM event — affilfinder:decline-session. Augment the window type so your event handlers are typed:
// types/affilfinder.d.ts
interface AffilFinderDeclineSessionDetail {
sessionId: string;
}
declare global {
interface WindowEventMap {
"affilfinder:decline-session": CustomEvent<AffilFinderDeclineSessionDetail>;
}
interface Window {
__AFFILFINDER_DECLINE_SESSION_ID?: string;
}
}
export {};6. Decline flow — full example
Server route for triggering
// app/api/affilfinder/trigger/route.ts
import { NextResponse } from "next/server";
const API = process.env.AFFILFINDER_API_URL ?? "https://api.affilfinder.com";
export async function POST(req: Request) {
const secret = process.env.AFFILFINDER_INTEGRATION_SECRET;
if (!secret) {
return NextResponse.json(
{ error: "AFFILFINDER_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/affilfinder/trigger", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId }),
});
const handler = (ev: WindowEventMap["affilfinder:decline-session"]) => {
void send(ev.detail.sessionId);
};
window.addEventListener("affilfinder:decline-session", handler);
const existing = window.__AFFILFINDER_DECLINE_SESSION_ID;
if (existing) void send(existing);
return () =>
window.removeEventListener("affilfinder: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="affilfinder-decline"
src="https://cdn.affilfinder.com/widget-decline.js"
strategy="beforeInteractive"
data-pub={process.env.NEXT_PUBLIC_AFFILFINDER_PUB}
data-website={process.env.NEXT_PUBLIC_AFFILFINDER_WEBSITE}
/>
<DeclineBridge />
<main className="prose mx-auto py-20">
<h1>We can't proceed with this account</h1>
<p>You may see alternative offers while we review.</p>
</main>
</>
);
}7. Preview / staging hosts
Each AffilFinder 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.appas the domain, use its keys on preview deploys, and keep production keys isolated. -
Point
data-apiat a staging API if you have one, so preview traffic doesn't pollute production analytics:tsx<Script src="https://cdn.affilfinder.com/widget.js" data-pub={process.env.NEXT_PUBLIC_AFFILFINDER_PUB} data-website={process.env.NEXT_PUBLIC_AFFILFINDER_WEBSITE} data-api={process.env.NEXT_PUBLIC_AFFILFINDER_API_URL} strategy="afterInteractive" />
Gotchas
- Middleware redirects — If your app redirects based on geo at the edge, make sure AffilFinder'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.
Related
Need more help?
Can't find what you're looking for? Our team responds within one business day.