Next.js 15 App Router (parityedge)
Add dynamic country discounts to your Next.js 15 App Router project in under 3 minutes. Show automatic localized banners to visitors in emerging markets and prefill discounts on Stripe or Lemon Squeezy checkout links without any page flicker or layout shifts.
Installation
npm package ↗npm install parityedgeClient Component Integration (App Router)
Choose one of two patterns depending on whether you need access to parity data in your own components.
Pattern A — Banner only (no custom UI needed)
Drop <ParityBanner> directly into any Client Component. It handles its own fetch internally — no provider required.
// app/pricing/page.tsx
'use client';
import { ParityBanner } from 'parityedge/react';
export default function PricingPage() {
return (
<main className="max-w-4xl mx-auto py-12 px-4">
<ParityBanner
projectId="prj_live_YOUR_PROJECT_ID"
theme="dark"
position="bottom-pill"
/>
<h1 className="text-3xl font-bold">Pro License — $49 USD</h1>
</main>
);
}Pattern B — Custom UI with parity data (ParityProvider + useParityEdge)
Wrap your tree in <ParityProvider>, then call useParityEdge() in any child component to read the resolved data and build your own UI.
// app/layout.tsx (or a shared client wrapper)
'use client';
import { ParityProvider } from 'parityedge/react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html><body>
<ParityProvider projectId="prj_live_YOUR_PROJECT_ID">
{children}
</ParityProvider>
</body></html>
);
}
// app/pricing/page.tsx
'use client';
import { useParityEdge } from 'parityedge/react';
export default function PricingPage() {
const { data } = useParityEdge();
const basePrice = 49;
const finalPrice = data?.eligible
? basePrice * (1 - data.discountPercentage / 100)
: basePrice;
return (
<main className="max-w-4xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold">Pro License — ${finalPrice.toFixed(2)} USD</h1>
{data?.eligible && (
<p className="text-emerald-400 mt-2 font-medium">
{data.flag} Use code <strong>{data.couponCode}</strong> at checkout.
</p>
)}
</main>
);
}<ParityBanner> and useParityEdge() together causes two separate API calls to the edge — wasting quota and doubling rate-limit exposure. Use Pattern A for banner-only, or Pattern B for custom UI. useParityEdge() must always be inside a <ParityProvider> tree.Edge Middleware for Server-Side Pre-Rendering
Use createParityMiddleware from parityedge/next to resolve visitor parity at the edge and stamp x-parity-* headers on every matching request — zero client-side waterfall:
// middleware.ts
import { createParityMiddleware } from 'parityedge/next';
export default createParityMiddleware({
projectId: process.env.NEXT_PUBLIC_PARITYEDGE_PROJECT_ID!,
// timeoutMs: 3000, // optional — default 3 s, fails open on timeout
});
export const config = {
matcher: ['/pricing', '/checkout/:path*'],
};Then read the stamped headers in any Server Component, Route Handler, or Server Action:
// app/pricing/page.tsx (Server Component)
import { headers } from 'next/headers';
import { getParityHeaders } from 'parityedge/next';
export default async function PricingPage() {
const parity = getParityHeaders(await headers());
const basePrice = 49;
const finalPrice = parity.isEligible
? basePrice * (1 - parity.discountPercentage / 100)
: basePrice;
return (
<main>
<h1>Pro License — ${finalPrice.toFixed(2)} USD</h1>
{parity.isEligible && (
<p>Use code <strong>{parity.couponCode}</strong> at checkout.</p>
)}
</main>
);
}req.geo?.country — that is a Vercel-only deprecated field and does not call the ParityEdge API. Always use createParityMiddleware.