Install on Next.js Pages Router
Use next/script in pages/_app.tsx. Don't paste raw script tags in _document.tsx, they conflict with serialization. Convert fires from useEffect in a success page using useRouter.
The Pages Router (Next.js's pre-App-Router pattern, still supported through Next 16) installs Rekomi via the next/script component in _app.tsx so the loader runs on every page render. Don't paste raw <script> tags in _document.tsx — that conflicts with how Pages Router serializes documents in Next 14+. The convert event fires from useEffect in your success page using useRouter for query params.
Install in pages/_app.tsx
Edit pages/_app.tsx and add the <Script> component inside the App component's return:
import type { AppProps } from "next/app";
import Script from "next/script";
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<Script
id="rekomi"
src="https://js.rekomi.com/v1/rekomi.js"
strategy="afterInteractive"
data-program-id={process.env.NEXT_PUBLIC_REKOMI_PROGRAM_ID}
/>
</>
);
}strategy="afterInteractive" defers loading until after hydration, which is the right balance for click capture.
Set NEXT_PUBLIC_REKOMI_PROGRAM_ID in .env.local (and your hosting env vars). The NEXT_PUBLIC_ prefix is required for browser exposure.
Why not _document.tsx?
In older Next.js (pre-14), _document.tsx was a common spot for third-party scripts via <Head> content. In Next 14+, the Pages Router's hydration pipeline can race with raw script tags in _document.tsx, causing the loader to run before React has finished hydrating. Symptoms: window.Rekomi undefined when client components try to call it, double-firing convert events on fast networks.
next/script in _app.tsx solves this by integrating with Next's hydration lifecycle. Stick with _app.tsx.
Fire convert from a success page
On pages/success.tsx (or whichever route is your post-checkout destination):
import { useEffect } from "react";
import { useRouter } from "next/router";
export default function SuccessPage() {
const router = useRouter();
useEffect(() => {
const sessionId = router.query.session_id as string;
if (!sessionId) return;
const amount = parseInt((router.query.amount as string) || "0", 10);
const email = (router.query.email as string) || "";
if (window.Rekomi) {
window.Rekomi.convert({
external_event_id: sessionId,
amount_cents: amount,
currency: "USD",
customer_email: email,
});
}
}, [router.query]);
return <div>Thanks for your purchase!</div>;
}useRouter().query exposes URL params parsed by Next. The useEffect runs after the page mounts, by which time window.Rekomi is hydrated.
For production, prefer fetching the canonical order data from your backend (a /api/checkout-session/[id] route) rather than trusting URL params — a buyer could append ?amount=1 to fake a refund-sized commission.
Stripe Checkout from a Pages Router app
The standard pattern: a Pages API route at pages/api/create-checkout-session.ts creates the Stripe Session server-side, then redirects the browser to the Stripe-hosted URL. The Rekomi referral travels via an HTTP header from the browser to your API route:
// pages/api/create-checkout-session.ts
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(405).end();
const referral = req.headers["x-rekomi-referral"] as string | undefined;
const { priceId } = req.body;
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
client_reference_id: referral,
subscription_data: referral
? { metadata: { rekomi_affiliate_slug: referral } }
: undefined,
});
res.json({ url: session.url });
}The browser-side code that calls this endpoint reads the referral from window.Rekomi.getReferral() and forwards it as a header. See the Install on Stripe doc for the full client-side pattern.
Quirks worth knowing
next/script belongs in _app.tsx, not _document.tsx. Don't follow older Stack Overflow answers that put scripts in _document.tsx <Head>. That pattern fights Next 14+'s hydration in subtle ways.
strategy="afterInteractive" is the right choice. Same as App Router — load after hydration but before idle.
router.query is empty on first render. Pages Router's router.query is populated AFTER the page mounts. The useEffect with [router.query] dep array re-runs once params are available; that's why convert fires from useEffect, not from the component body.
Image domains config. If you load OG images from Rekomi, add js.rekomi.com and api.rekomi.com to next.config.js's images.domains array.
getServerSideProps can also pass the referral. If your success page uses SSR, you can read the cookie via context.req.cookies.rekomi_via in getServerSideProps and pass it as a prop. Useful if you want to verify the referral server-side before showing the success state.
Troubleshooting
window.Rekomi is undefined when calling from useEffect. The Script hasn't loaded yet. Move the convert call into a setTimeout(..., 100) or check window.Rekomi existence before calling (the if (window.Rekomi) guard above handles this).
Convert fires twice on the success page. The useEffect is re-running because router.query changes from {} to populated. Add a guard: only fire if sessionId is present and you haven't already fired (use a useRef to track).
API route returns 500. The STRIPE_SECRET_KEY env var is missing. Set it in .env.local and your hosting env vars.
Script tag appears but no cookie set. CSP blocking. Check the browser console for CSP violations and adjust next.config.js headers.
Related
- JavaScript pixel reference
- Install on Stripe — for the client-side referral capture pattern
- Install on Next.js App Router — the modern pattern; consider migrating
Install on Next.js App Router
Use next/script in app/layout.tsx with strategy="afterInteractive". Raw script tags in metadata get stripped. Server Actions can pass the affiliate slug via cookies().
Install on React (Vite, CRA, Remix)
Vite and CRA install in index.html; Remix and React Router 7 install in app/root.tsx. SPA navigation doesn't reload pages, so convert fires from a useEffect in your success route.