Skip to main content

Custom · Nuxt 3

This guide covers Nuxt 3 with the Nitro server engine. The pattern follows the same idea as Next.js:

  1. Private runtimeConfig for shopId, apiKey, apiUrl.
  2. Server route (server/api/neuroon-token.get.ts) that requests the Widget Token and caches it in memory.
  3. 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 cache with useStorage('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.ts suffix 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