Custom · Nuxt 3
This guide covers Nuxt 3 with the Nitro server engine. The pattern follows the same idea as Next.js:
- Private
runtimeConfigforshopId,apiKey,apiUrl. - Server route (
server/api/neuroon-token.get.ts) that requests the Widget Token and caches it in memory. - Client Nuxt plugin (
plugins/neuroon.client.ts) that loads the CDN loader.
Configuration
// 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',
// Client (do not include the 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';
// Widget Token TTL is fixed at 24 h. Rotate at 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 };
});
In Nitro serverless (Vercel, Netlify), replace the
let cachewithuseStorage('redis')or an equivalent shared cache.
Client plugin
// 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 ?? 'en';
s.dataset.apiUrl = cfg.neuroonApiUrl as string;
document.head.appendChild(s);
}
if (document.readyState === 'complete') injectLoader();
else window.addEventListener('load', injectLoader, { once: true });
});
The
.client.tssuffix ensures the plugin only runs in the browser.
Reusable component
To anchor the widget in a specific section of the 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
If your store uses 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 },
);
}
And from app.vue:
<script setup lang="ts">
useNeuroonCartBridge();
</script>
Conversion tracking
// 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 };
});
Next steps
server-to-server— cross-stack token issuance pattern.- Recipe · Server-to-server token.
- Widget · Cart integration.