Custom · Next.js
Esta guía cubre Next.js 14+ y Next.js 16 con App Router. El patrón es:
- Variables de entorno server-only para
NEUROON_SHOP_IDyNEUROON_API_KEY. - Route Handler (
app/api/neuroon-token/route.ts) que pide el Widget Token a Neuroon y lo cachea. <Script>cliente que carga el loader del CDN constrategy="afterInteractive".
Variables de entorno
# .env.local (NUNCA committear)
NEUROON_SHOP_ID=shop_xxxxxxxx
NEUROON_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NEUROON_API_URL=https://api.neuroon.ai
Las tres no llevan el prefijo
NEXT_PUBLIC_. Quedan server-only y nunca llegan al bundle del cliente. La Shop API Key se usa para firmar el Widget Token localmente (HMAC) y para autenticar las llamadas server-to-server (/products/sync,/track/conversion).
Route Handler que firma el token
// 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 cache: { 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 raw = `${payload}:${sig}`;
const token = Buffer.from(raw, 'utf8').toString('base64url');
return { token, expiresAt: (ts + 24 * 3600) * 1000 };
}
export async function GET() {
const now = Date.now();
// Re-firma con margen de 5 min antes de expirar
if (!cache || now >= cache.expiresAt - 5 * 60_000) {
cache = signToken();
}
return NextResponse.json({ token: cache.token, expiresAt: cache.expiresAt });
}
En entornos serverless (Vercel) el
let cacheno persiste entre invocaciones frías. Usa Redis (@upstash/redis) o Vercel KV para una cache compartida entre instancias. Ver Recipe · Server-to-server token para la variante distribuida.
Componente del widget (Client)
// app/components/NeuroonSearch.tsx
'use client';
import { useEffect, useState } from 'react';
import Script from 'next/script';
export function NeuroonSearch() {
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
fetch('/api/neuroon-token', { credentials: 'include' })
.then((r) => r.json())
.then((d) => setToken(d.token))
.catch(console.error);
}, []);
if (!token) {
return <div id="neuroon-search" aria-busy="true" />;
}
return (
<>
<div id="neuroon-search" />
<Script
src="https://cdn.neuroon.ai/widget@0.9.10/widget.js"
strategy="afterInteractive"
integrity="sha384-JTaG/IN0Jj/ImfUj2x5QVMG4HkbFHzui7fTpLtwl1hsP+kY9W8OODeSJRFWN1ZP5"
crossOrigin="anonymous"
data-token={token}
data-container="#neuroon-search"
data-theme="auto"
data-locale="es"
data-api-url={process.env.NEXT_PUBLIC_NEUROON_API_URL ?? 'https://api.neuroon.ai'}
/>
</>
);
}
Inyección desde el layout (RSC)
Para imprimir el token directamente en el HTML SSR (mejor LCP, sin flash):
// app/layout.tsx (Server Component)
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="es">
<body>
{children}
<div id="neuroon-search" />
<Script
src="https://cdn.neuroon.ai/widget@0.9.10/widget.js"
strategy="afterInteractive"
integrity="sha384-JTaG/IN0Jj/ImfUj2x5QVMG4HkbFHzui7fTpLtwl1hsP+kY9W8OODeSJRFWN1ZP5"
crossOrigin="anonymous"
data-token={token}
data-container="#neuroon-search"
data-theme="auto"
data-locale="es"
/>
</body>
</html>
);
}
La cache tokenCache por instancia de Node garantiza que la firma se ejecuta una vez cada ~24 h. En multi-pod (Vercel) usa Redis/KV — ver Recipe · Server-to-server token.
Cart bridge en SPA
Si tu store maneja el carrito en estado cliente (Zustand, Redux, Context), emite el evento desde un effect:
'use client';
import { useEffect } from 'react';
import { useCartStore } from '@/store/cart';
export function CartBridge() {
const cart = useCartStore((s) => s.snapshot());
useEffect(() => {
window.dispatchEvent(new CustomEvent('neuroon:cart-update', { detail: cart }));
}, [cart]);
return null;
}
Renderiza <CartBridge /> en app/layout.tsx para que escuche cambios globales.
Tracking de conversiones
En Next.js, llama al endpoint de conversión desde tu Route Handler de "order confirmed":
// app/api/orders/[id]/confirm/route.ts
export async function POST(req: Request, { params }: { params: { id: string } }) {
const order = await markOrderAsPaid(params.id);
await fetch(`${process.env.NEUROON_API_URL}/api/plugin/shops/${process.env.NEUROON_SHOP_ID}/track/conversion`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shop-API-Key': process.env.NEUROON_API_KEY!,
},
body: JSON.stringify({
orderId: order.id,
orderValue: order.total,
currency: order.currency,
conversions: order.lines.map((l) => ({
productId: l.sku, quantity: l.quantity, lineTotal: l.unitPrice * l.quantity,
})),
}),
});
return Response.json({ ok: true });
}
Próximos pasos
- Recipe · Next.js + React integration — versión guiada paso a paso.
server-to-server— patrón general de emisión del token.- Widget · Configuration.