Skip to content
GitHub
Get started →

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.

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

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

  1. npm run dev
  2. Open http://localhost:3000
  3. Orb at the bottom center. Works.
  4. Click orb → allow mic → speak.

Troubleshooting

  • Widget disappears on route change → confirm the <Script> is in app/layout.tsx or pages/_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-id is correct. Server-side environment variables do not reach next/script; use NEXT_PUBLIC_*.