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 Shop 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.10/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/plugin/shops/${process.env.NEUROON_SHOP_ID}/products/sync`,
{
method: 'POST',
headers: {
'X-Shop-API-Key': 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/plugin/shops/${process.env.NEUROON_SHOP_ID}/track/conversion`,
{
method: 'POST',
headers: {
'X-Shop-API-Key': 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-api.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