Saltar al contenido principal

Custom · Nuxt 3

Esta guía cubre Nuxt 3 con Nitro server engine. El patrón sigue la misma idea que Next.js:

  1. runtimeConfig privado para shopId, apiKey, apiUrl.
  2. Server route (server/api/neuroon-token.get.ts) que pide el Widget Token y lo cachea en memoria.
  3. 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 cache por useStorage('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.ts garantiza 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