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 mints the Widget Token

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

import { NextResponse } from 'next/server';

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

// Widget Token TTL is fixed at 24 h. Rotate at 23 h.
const ROTATE_AFTER_MS = 23 * 60 * 60 * 1000;

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

export async function GET() {
const now = Date.now();
if (cached && now - cached.issuedAt < ROTATE_AFTER_MS) {
return NextResponse.json({ token: cached.token });
}

const res = await fetch(
`${process.env.NEUROON_API_URL}/api/shops/${process.env.NEUROON_SHOP_ID}/widget-token`,
{
method: 'POST',
headers: { 'X-Shop-API-Key': process.env.NEUROON_API_KEY! },
},
);

if (!res.ok) {
return NextResponse.json({ error: 'Failed to mint token' }, { status: 502 });
}

const { token } = (await res.json()) as { token: string };
cached = { token, issuedAt: now };

return NextResponse.json({ token });
}

3. Embed the widget in the root layout

app/layout.tsx:

import Script from 'next/script';

async function getWidgetToken(): Promise<string> {
const baseUrl = process.env.NEUROON_API_URL!;
const res = await fetch(
`${baseUrl}/api/shops/${process.env.NEUROON_SHOP_ID}/widget-token`,
{
method: 'POST',
headers: { 'X-Shop-API-Key': process.env.NEUROON_API_KEY! },
next: { revalidate: 82_800 }, // 23h
},
);
if (!res.ok) throw new Error('Could not mint widget token');
const { token } = await res.json();
return token;
}

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

return (
<html lang="en">
<body>
{children}
<div id="neuroon-search" />
<Script
src="https://cdn.neuroon.ai/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 token is cached with next: { revalidate: 82800 } so subsequent requests reuse it for 23 hours (Next handles the cache automatically).

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; searchLogId: 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,
searchLogId: i.searchLogId, // required (@NotBlank on ConversionItem)
quantity: i.quantity,
lineTotal: i.lineTotal,
})),
}),
},
);
}

searchLogId is required: the widget emits it on every track:click and stores it in a cookie. On the client, read the cookie and map each SKU to its searchLogId before sending the order to the server. Without it the API responds 400. See Recipe · Conversion tracking.

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

Switch the API URL and replace the key/shopId with the ones from the Production environment (you get them at neuroon.ai/dashboard). Keys follow the same format sk_<32hex> in both environments but are independent:

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

# Replace the DEV key with the PROD one (same format sk_<32hex>)
NEUROON_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NEUROON_SHOP_ID=shop_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

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

Next steps