Saltar al contenido principal

Next.js / React

Esta receta integra Neuroon en una app Next.js 14+ (App Router). El patrón es el mismo para cualquier framework React server-rendered.

Estructura final

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

1. Variables de entorno

.env.local:

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

Server-only (sin prefijo NEXT_PUBLIC_). Si no tienes la API Key, obtenla aquí.

2. Route Handler que emite el Widget Token

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

import { NextResponse } from 'next/server';

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

// TTL fijo del Widget Token: 24 h. Rotamos a las 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. Embebido del widget en el 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="es">
<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="es"
strategy="afterInteractive"
/>
</body>
</html>
);
}

El Script con strategy="afterInteractive" se inyecta tras la hidratación inicial de Next, no bloquea el First Paint. El token se cachea con next: { revalidate: 82800 } para reaprovechar tokens entre requests durante 23 horas (Next maneja la caché automáticamente).

4. Sincroniza productos desde un Server Action o 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;
};
}

Llámalo desde donde tengas tu catálogo (cron job con inngest/vercel-cron, webhook de tu CMS, Server Action manual). El backend acepta hasta 500 productos por batch.

5. Tracking de conversiones server-side

Dentro del Server Action que confirma el pedido:

'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, // requerido (@NotBlank en ConversionItem)
quantity: i.quantity,
lineTotal: i.lineTotal,
})),
}),
},
);
}

searchLogId es obligatorio: el widget lo emite en cada track:click y lo guarda en una cookie. En el cliente, lee la cookie y mapea cada SKU a su searchLogId antes de enviar el pedido al servidor. Sin él la API responde 400. Ver Recipe · Conversion tracking.

Conversiones server-side son resistentes a adblockers (a diferencia del tracking del navegador).

Comprobación rápida

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

# 2. Carga la home y haz click en el widget
open http://localhost:3000

Si el widget no aparece, revisa Troubleshooting.

Producción

Cambia la URL de la API y reemplaza la key/shopId por los del entorno de Producción (los obtienes en neuroon.ai/dashboard). Las keys tienen el mismo formato sk_<32hex> en ambos entornos pero son independientes:

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

# Reemplaza la key de DEV por la de PROD (mismo formato sk_<32hex>)
NEUROON_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NEUROON_SHOP_ID=shop_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

El CDN del widget (cdn.neuroon.ai/widget.js) es el mismo en ambos entornos.

Siguientes pasos