React and Next.js
The widget is a plain script — it does not care what framework rendered the page. The only thing to get right is loading it once.
Next.js — App Router
Add it to your root layout using the built-in Script component:
import Script from 'next/script';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Script id="sitemesh-widget" strategy="afterInteractive">
{`
window.ChatbotConfig = {
chatbotId: "YOUR_CHATBOT_ID",
apiKey: "YOUR_WIDGET_KEY",
apiUrl: "https://apimesh.byteleaps.tech/api"
};
var s = document.createElement('script');
s.src = 'https://apimesh.byteleaps.tech/widget.js';
s.async = true;
document.head.appendChild(s);
`}
</Script>
</body>
</html>
);
}
The id matters: it is how Next deduplicates the script across navigations.
Next.js — Pages Router
Same idea, in _app.tsx or _document.tsx:
import Script from 'next/script';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<Script id="sitemesh-widget" strategy="afterInteractive">
{`
window.ChatbotConfig = { /* ... */ };
var s = document.createElement('script');
s.src = 'https://apimesh.byteleaps.tech/widget.js';
s.async = true;
document.head.appendChild(s);
`}
</Script>
</>
);
}
Plain React (Vite, CRA)
The simplest approach is not React at all — put the snippet directly in
index.html, before </body>:
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script>
(function() {
window.ChatbotConfig = {
chatbotId: "YOUR_CHATBOT_ID",
apiKey: "YOUR_WIDGET_KEY",
apiUrl: "https://apimesh.byteleaps.tech/api"
};
var s = document.createElement('script');
s.src = 'https://apimesh.byteleaps.tech/widget.js';
s.async = true;
document.head.appendChild(s);
})();
</script>
</body>
This loads once, outside React's lifecycle entirely, which sidesteps every double-mount problem below.
Loading it from a component
If you must load it from React — because the key comes from runtime config, say — guard against duplicate injection:
import { useEffect } from 'react';
const SCRIPT_ID = 'sitemesh-widget';
export function SiteMeshWidget() {
useEffect(() => {
// React 18+ StrictMode runs effects twice in development. Without this
// guard the widget script is injected twice and you get two bubbles.
if (document.getElementById(SCRIPT_ID)) return;
(window as any).ChatbotConfig = {
chatbotId: import.meta.env.VITE_SITEMESH_CHATBOT_ID,
apiKey: import.meta.env.VITE_SITEMESH_WIDGET_KEY,
apiUrl: import.meta.env.VITE_SITEMESH_API_URL,
};
const s = document.createElement('script');
s.id = SCRIPT_ID;
s.src = 'https://apimesh.byteleaps.tech/widget.js';
s.async = true;
document.head.appendChild(s);
}, []);
return null;
}
Render it once, high in the tree — in your root layout, not inside a route component that unmounts.
That is the widget loading twice. In development it is usually React StrictMode
running your effect twice; in production it is usually the component being
rendered in more than one place. The getElementById guard above fixes both.
Client-side routing
The widget attaches to the page, not to a route. Navigating between routes in a single-page app does not unload it and does not require anything from you.
Do not try to mount and unmount it per route — that produces flicker and lost conversation state.
Vue, Nuxt, Svelte, Astro
Same principle: put the snippet in the root HTML template.
| Framework | File |
|---|---|
| Vue (Vite) | index.html |
| Nuxt | app.vue, or nuxt.config.ts under app.head.script |
| SvelteKit | src/app.html |
| Astro | Your base layout .astro file |
Environment variables
Keep your chatbot ID and widget key in environment variables rather than hard-coding them, so staging and production can use different keys.
Note that these variables end up in the client bundle — that is expected and fine. The widget key is a public, domain-restricted credential by design. See Are widget keys safe?
Content Security Policy
If your site sends a CSP header, allow the widget's script source and its API origin. Without this the browser blocks the script and you will see a CSP violation in the console.
It is not showing up
- Open the console and look for
SiteMesh Widget: ChatbotConfig missing chatbotId or apiKey— that means an environment variable did not resolve at build time - Check the script tag actually made it into the DOM
- Check your domain is in the key's
allowed domains —
localhostis a different hostname from your production domain