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 } from 'h3';
import { createHmac } from 'node:crypto';
let cache: { token: string; expiresAt: number } | null = null;
function signToken(shopId: string, apiKey: string): { token: string; expiresAt: number } {
const ts = Math.floor(Date.now() / 1000);
const payload = `${shopId}:${ts}`;
const sig = createHmac('sha256', apiKey).update(payload).digest('hex');
const token = Buffer.from(`${payload}:${sig}`, 'utf8').toString('base64url');
return { token, expiresAt: (ts + 24 * 3600) * 1000 };
}
export default defineEventHandler(async (_event) => {
const cfg = useRuntimeConfig();
const now = Date.now();
if (!cache || now >= cache.expiresAt - 5 * 60_000) {
cache = signToken(cfg.neuroonShopId as string, cfg.neuroonApiKey as string);
}
return { token: cache.token, expiresAt: cache.expiresAt };
});
En Nitro serverless (Vercel, Netlify), reemplaza el
let cacheporuseStorage('redis')o un cache compartido equivalente. Ver Recipe · Server-to-server token para la variante distribuida.
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.