Saltar al contenido principal

Custom · Next.js

Esta guía cubre Next.js 14+ y Next.js 16 con App Router. El patrón es:

  1. Variables de entorno server-only para NEUROON_SHOP_ID y NEUROON_API_KEY.
  2. Route Handler (app/api/neuroon-token/route.ts) que pide el Widget Token a Neuroon y lo cachea.
  3. <Script> cliente que carga el loader del CDN con strategy="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 dos primeras no llevan el prefijo NEXT_PUBLIC_. Quedan server-only y nunca llegan al bundle del cliente.

Route Handler para el 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 cache: { token: string; issuedAt: number } | null = null;

async function fetchToken(): Promise<{ token: string; issuedAt: number }> {
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!,
'User-Agent': 'nextjs-neuroon/1.0',
},
cache: 'no-store',
},
);
if (!res.ok) throw new Error(`Neuroon token issuance failed: ${res.status}`);
const body = (await res.json()) as { token: string };
return { token: body.token, issuedAt: Date.now() };
}

export async function GET() {
const now = Date.now();
if (!cache || now - cache.issuedAt >= ROTATE_AFTER_MS) {
cache = await fetchToken();
}
return NextResponse.json({ token: cache.token });
}

En entornos serverless (Vercel) el let cache no persiste entre invocaciones frías. Usa Redis (@upstash/redis) o Vercel KV para una cache compartida entre instancias.

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';

async function getWidgetToken(): Promise<string> {
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! },
next: { revalidate: 60 * 60 * 12 }, // 12 h ISR cache
},
);
const body = await res.json();
return body.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@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>
);
}

next: { revalidate: 60 * 60 * 12 } aprovecha la cache HTTP/ISR de Next.js: el endpoint solo se golpea cada 12 h por instancia.

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