Skip to main content

Next.js / React

This recipe integrates Neuroon in a Next.js 14+ app (App Router). The pattern is the same for any server-rendered React framework.

Final structure​

app/
├── api/
│ └── neuroon-token/
│ └── route.ts # Mints the Widget Token
├── layout.tsx # Embeds the widget
└── page.tsx
.env.local # NEUROON_API_KEY, NEUROON_SHOP_ID

1. Environment variables​

.env.local:

NEUROON_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NEUROON_SHOP_ID=shop_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
NEUROON_API_URL=https://api.neuroon.ai

Server-only (no NEXT_PUBLIC_ prefix). If you don't have an API Key yet, get it here.

2. Route Handler that signs the Widget Token​

Your server signs the token locally with the API Key as HMAC secret. You do not call Neuroon to issue it.

app/api/neuroon-token/route.ts:

import { NextResponse } from 'next/server';
import { createHmac } from 'node:crypto';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

let cached: { token: string; expiresAt: number } | null = null;

function signToken(): { token: string; expiresAt: number } {
const ts = Math.floor(Date.now() / 1000);
const payload = `${process.env.NEUROON_SHOP_ID}:${ts}`;
const sig = createHmac('sha256', process.env.NEUROON_API_KEY!).update(payload).digest('hex');
const token = Buffer.from(`${payload}:${sig}`, 'utf8').toString('base64url');
return { token, expiresAt: (ts + 24 * 3600) * 1000 };
}

export async function GET() {
const now = Date.now();
if (!cached || now >= cached.expiresAt - 5 * 60_000) {
cached = signToken();
}
return NextResponse.json({ token: cached.token });
}

Full pattern (Node, .NET, Python, PHP) with distributed Redis cache in Recipe · Server-to-server token.

3. Embed the widget in the root layout​

app/layout.tsx:

import Script from 'next/script';
import { createHmac } from 'node:crypto';

let tokenCache: { token: string; expiresAt: number } | null = null;

function getWidgetToken(): string {
const now = Date.now();
if (tokenCache && now < tokenCache.expiresAt - 5 * 60_000) return tokenCache.token;
const ts = Math.floor(now / 1000);
const payload = `${process.env.NEUROON_SHOP_ID}:${ts}`;
const sig = createHmac('sha256', process.env.NEUROON_API_KEY!).update(payload).digest('hex');
const token = Buffer.from(`${payload}:${sig}`, 'utf8').toString('base64url');
tokenCache = { token, expiresAt: (ts + 24 * 3600) * 1000 };
return token;
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
const token = getWidgetToken();

return (
<html lang="en">
<body>
{children}
<div id="neuroon-search" />
<Script
src="https://cdn.neuroon.ai/widget@0.9.27/widget.js"
data-token={token}
data-container="#neuroon-search"
data-theme="auto"
data-locale="en"
strategy="afterInteractive"
/>
</body>
</html>
);
}

The Script with strategy="afterInteractive" is injected after Next's initial hydration; it doesn't block First Paint. The per-instance tokenCache ensures signing runs at most once every ~24 h. In multi-pod (Vercel) use Redis/KV.

4. Sync products from a Server Action or cron​

app/actions/sync-products.ts:

'use server';

export async function syncProducts(products: Array<{
externalId: string;
name: string;
price: number;
currency: string;
url: string;
}>) {
const res = await fetch(
`${process.env.NEUROON_API_URL}/api/integrations/v1/products/sync`,
{
method: 'POST',
headers: {
'Authorization': process.env.NEUROON_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ syncType: 'INCREMENTAL', products }),
},
);

if (!res.ok) {
throw new Error(`Sync failed: ${res.status}`);
}

return (await res.json()) as {
totalReceived: number;
newProducts: number;
updatedProducts: number;
failed: number;
};
}

Call it from wherever you have your catalog (a cron job with inngest/vercel-cron, a CMS webhook, a manual Server Action). The backend accepts up to 500 products per batch.

5. Server-side conversion tracking​

Inside the Server Action that confirms the order:

'use server';

export async function confirmOrder(order: {
id: string;
total: number;
currency: string;
items: Array<{ sku: string; quantity: number; lineTotal: number }>;
}) {
await fetch(
`${process.env.NEUROON_API_URL}/api/integrations/v1/conversions`,
{
method: 'POST',
headers: {
'Authorization': process.env.NEUROON_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
orderId: order.id,
orderValue: order.total,
currency: order.currency,
conversions: order.items.map((i) => ({
productId: i.sku,
quantity: i.quantity,
lineTotal: i.lineTotal,
})),
}),
},
);
}

Server-side conversions are resilient to adblockers (unlike browser tracking).

Quick check​

# 1. Token endpoint works
curl http://localhost:3000/api/neuroon-token

# 2. Load home and click the widget
open http://localhost:3000

If the widget doesn't appear, see Troubleshooting.

Production​

You only change two variables when going to production:

- NEUROON_API_URL=https://dev.neuroon.ai
+ NEUROON_API_URL=https://api.neuroon.ai

- NEUROON_API_KEY=sk_dev_xxxxxxxxxxxxxxxxxxxxxxxx
+ NEUROON_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

The widget CDN (cdn.neuroon.ai/widget@<version>/widget.js) is the same in both environments.

Next steps​