Install on Next.js
Next.js 13 and up (both App Router and Pages Router) ships a <Script> component that loads third-party scripts with sensible defaults. Use it.
Add the widget to your root layout so it renders on every page.
import Script from 'next/script'
export default function RootLayout({ children,}: { children: React.ReactNode}) { return ( <html lang="en"> <body> {children} <Script src="https://spelo.ai/spelo.js" data-site-id="YOUR_SITE_ID" strategy="afterInteractive" /> </body> </html> )}The afterInteractive strategy loads the widget after hydration — the right default for the voice orb. Do not use beforeInteractive (that strategy is only for critical scripts).
Add the widget to pages/_app.tsx (preferred) or pages/_document.tsx.
import type { AppProps } from 'next/app'import Script from 'next/script'
export default function App({ Component, pageProps }: AppProps) { return ( <> <Component {...pageProps} /> <Script src="https://spelo.ai/spelo.js" data-site-id="YOUR_SITE_ID" strategy="afterInteractive" /> </> )}Route changes and the singleton
Next.js client-side routing (the Link component) does not cause a full page reload, so the widget script only loads once. The widget is built for this — it maintains a singleton WebRTC connection and scrapes the new DOM after each history.pushState. No extra config required.
Hydration notes
The widget injects a root element into document.body during DOMContentLoaded. It uses Shadow DOM, so it cannot cause React hydration mismatches (React only hydrates the server-rendered tree, and the widget lives outside it).
If you see a hydration warning that mentions the widget, it usually means a CSS-in-JS library is trying to manage document.body. Move the widget’s mount to a different target via the data-target attribute:
<Script src="https://spelo.ai/spelo.js" data-site-id="YOUR_SITE_ID" data-target="#spelo-root" strategy="afterInteractive"/>Then render <div id="spelo-root" /> at the top level of your layout.
Next.js + Vercel deployment
No special config. The widget works identically in local next dev, next build && next start, and Vercel serverless.
Middleware and edge
The widget runs entirely in the browser. Your middleware.ts cannot block it (and should not try). If you proxy responses through a middleware that rewrites HTML, make sure the <script> tag survives.
Environment variables for site_id
If you host multiple environments (dev/staging/prod) with different site IDs:
<Script src="https://spelo.ai/spelo.js" data-site-id={process.env.NEXT_PUBLIC_SPELO_SITE_ID} strategy="afterInteractive"/>Expose via NEXT_PUBLIC_* so the value is inlined at build time.
Content Security Policy
If you set a CSP (via next.config.js headers or a middleware), allow:
script-src 'self' https://spelo.ai;connect-src 'self' https://api.spelo.ai https://api.openai.com wss://*;media-src 'self' blob:;Verify
npm run dev- Open
http://localhost:3000 - Orb at the bottom center. Works.
- Click orb → allow mic → speak.
Troubleshooting
- Widget disappears on route change → confirm the
<Script>is inapp/layout.tsxorpages/_app.tsx, not a leaf page. - Hydration mismatch warnings → use
data-target(above) to move the mount outside React’s tree. - Works locally but 404s in production → check your
data-site-idis correct. Server-side environment variables do not reachnext/script; useNEXT_PUBLIC_*.