Custom · Nuxt 3
Esta guía cubre Nuxt 3 con Nitro server engine. El patrón sigue la misma idea que Next.js:
runtimeConfigprivado parashopId,apiKey,apiUrl.- Server route (
server/api/neuroon-token.get.ts) que pide el Widget Token y lo cachea en memoria. - Plugin Nuxt cliente (
plugins/neuroon.client.ts) que carga el loader del CDN.
Configuración
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Server-only
neuroonShopId: process.env.NEUROON_SHOP_ID,
neuroonApiKey: process.env.NEUROON_API_KEY,
neuroonApiUrl: process.env.NEUROON_API_URL ?? 'https://api.neuroon.ai',
// Cliente (no incluyas la API key)
public: {
neuroonApiUrl: process.env.NEUROON_API_URL ?? 'https://api.neuroon.ai',
neuroonCdn: 'https://cdn.neuroon.ai',
neuroonWidgetVersion: '0.9.10',
neuroonWidgetSri: 'sha384-JTaG/IN0Jj/ImfUj2x5QVMG4HkbFHzui7fTpLtwl1hsP+kY9W8OODeSJRFWN1ZP5',
},
},
});
Server route
// server/api/neuroon-token.get.ts
import { defineEventHandler, createError } from 'h3';
// 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;
export default defineEventHandler(async (_event) => {
const cfg = useRuntimeConfig();
const now = Date.now();
if (!cache || now - cache.issuedAt >= ROTATE_AFTER_MS) {
const res = await $fetch<{ token: string }>(
`${cfg.neuroonApiUrl}/api/shops/${cfg.neuroonShopId}/widget-token`,
{
method: 'POST',
headers: {
'X-Shop-API-Key': cfg.neuroonApiKey,
'User-Agent': 'nuxt-neuroon/1.0',
},
},
).catch((err) => {
throw createError({ statusCode: 502, message: `Neuroon token issuance failed: ${err.message}` });
});
cache = { token: res.token, issuedAt: now };
}
return { token: cache.token };
});
En Nitro serverless (Vercel, Netlify), reemplaza el
let cacheporuseStorage('redis')o un cache compartido equivalente.
Plugin cliente
// plugins/neuroon.client.ts
export default defineNuxtPlugin(async (nuxtApp) => {
if (process.server) return;
const cfg = useRuntimeConfig().public;
const { token } = await $fetch<{ token: string }>('/api/neuroon-token');
function injectLoader() {
if (!document.getElementById('neuroon-search')) {
const div = document.createElement('div');
div.id = 'neuroon-search';
document.body.appendChild(div);
}
const s = document.createElement('script');
s.src = `${cfg.neuroonCdn}/widget/widget.js`;
s.integrity = cfg.neuroonWidgetSri as string;
s.crossOrigin = 'anonymous';
s.async = true;
s.dataset.token = token;
s.dataset.container = '#neuroon-search';
s.dataset.theme = 'auto';
s.dataset.locale = nuxtApp.$i18n?.locale.value ?? 'es';
s.dataset.apiUrl = cfg.neuroonApiUrl as string;
document.head.appendChild(s);
}
if (document.readyState === 'complete') injectLoader();
else window.addEventListener('load', injectLoader, { once: true });
});
El sufijo
.client.tsgarantiza que el plugin solo corre en el navegador.
Componente reutilizable
Para anclar el widget en una sección concreta del layout:
<template>
<div id="neuroon-search" :aria-busy="!loaded" />
</template>
<script setup lang="ts">
const loaded = useState('neuroon-loaded', () => false);
onMounted(() => {
const observer = new MutationObserver(() => {
if (document.querySelector('#neuroon-search [data-neuroon-ready]')) {
loaded.value = true;
observer.disconnect();
}
});
observer.observe(document.getElementById('neuroon-search')!, { childList: true, subtree: true });
});
</script>
Cart bridge
Si tu store usa Pinia:
// composables/useNeuroonCartBridge.ts
import { storeToRefs } from 'pinia';
import { useCartStore } from '~/stores/cart';
export function useNeuroonCartBridge() {
if (process.server) return;
const cart = storeToRefs(useCartStore());
watch(
() => cart.snapshot.value,
(s) => window.dispatchEvent(new CustomEvent('neuroon:cart-update', { detail: s })),
{ deep: true },
);
}
Y desde app.vue:
<script setup lang="ts">
useNeuroonCartBridge();
</script>
Tracking de conversiones
// server/api/orders/[id]/confirm.post.ts
export default defineEventHandler(async (event) => {
const cfg = useRuntimeConfig();
const id = getRouterParam(event, 'id');
const order = await markOrderPaid(id!);
await $fetch(
`${cfg.neuroonApiUrl}/api/plugin/shops/${cfg.neuroonShopId}/track/conversion`,
{
method: 'POST',
headers: { 'X-Shop-API-Key': cfg.neuroonApiKey },
body: {
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,
})),
},
},
).catch((err) => console.warn('Neuroon conversion tracking failed', err));
return { ok: true };
});
Próximos pasos
server-to-server— patrón cross-stack de emisión de tokens.- Recipe · Server-to-server token.
- Widget · Cart integration.