Skip to main content

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:

app/layout.tsx
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:

pages/_app.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>:

index.html
<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:

components/SiteMeshWidget.tsx
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.

Two chat bubbles?

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.

FrameworkFile
Vue (Vite)index.html
Nuxtapp.vue, or nuxt.config.ts under app.head.script
SvelteKitsrc/app.html
AstroYour 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

  1. Open the console and look for SiteMesh Widget: ChatbotConfig missing chatbotId or apiKey — that means an environment variable did not resolve at build time
  2. Check the script tag actually made it into the DOM
  3. Check your domain is in the key's allowed domainslocalhost is a different hostname from your production domain

Widget troubleshooting