Install on Astro
Astro’s island architecture plays well with the widget. Drop a single <script> tag into your site-wide Layout and it runs on every page.
Steps
-
Open your layout — typically
src/layouts/Layout.astroor similar. -
Paste the snippet before
</body>:src/layouts/Layout.astro <html lang="en"><head><slot name="head" /></head><body><slot /><scriptis:inlinesrc="https://spelo.ai/spelo.js"data-site-id="YOUR_SITE_ID"async></script></body></html>The
is:inlinedirective tells Astro to emit the script as-is (not bundle it). -
Run
npm run dev→ the orb should appear on every page that uses this layout.
View Transitions
If you use Astro’s View Transitions (<ViewTransitions />), client-side navigation becomes seamless. The widget detects route changes via astro:after-swap:
---import { ViewTransitions } from 'astro:transitions'---<html> <head> <ViewTransitions /> </head> <body> <slot />
<script is:inline src="https://spelo.ai/spelo.js" data-site-id="YOUR_SITE_ID" async ></script> </body></html>The widget auto-listens for astro:after-swap events and re-reads the DOM. No extra configuration.
Static vs. SSR
The widget doesn’t care which Astro output mode you use.
output: 'static'(default) — prerendered HTML. The<script>tag is baked into every generated page.output: 'hybrid'— the same, plus any SSR’d pages also include the tag via the shared Layout.output: 'server'— every page is SSR’d; the tag is emitted at request time.
Environment variables
Astro exposes env vars via import.meta.env. For client-side code, prefix with PUBLIC_:
PUBLIC_SPELO_SITE_ID=abc123xy---const siteId = import.meta.env.PUBLIC_SPELO_SITE_ID---<script is:inline src="https://spelo.ai/spelo.js" data-site-id={siteId} async></script>Integration with Starlight (docs sites)
If you’re adding the widget to a Starlight site like this one, add a HeadBase override:
starlight({ components: { Head: './src/components/Head.astro', },})Then in src/components/Head.astro:
---import Default from '@astrojs/starlight/components/Head.astro'---<Default /><script is:inline src="https://spelo.ai/spelo.js" data-site-id="YOUR_SITE_ID" async></script>Integrations that should not conflict
@astrojs/tailwind— no conflict. The widget uses Shadow DOM, immune to Tailwind global styles.@astrojs/mdx— no conflict.@astrojs/partytown— do not route the Spelo script through Partytown. Partytown moves scripts off the main thread into a web worker, and the widget needs access tonavigator.mediaDevices, WebRTC, and the DOM. Excludespelo.jsfrom Partytown’s hookup.
CSP
Astro doesn’t set a CSP by default. If you do (via response headers or a reverse proxy):
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- Hit
http://localhost:4321 - Orb at the bottom; click → allow mic → speak
Troubleshooting
- Widget gone after View Transitions swap → confirm the
<script>tag is in your persistent Layout (the tag persists across swaps). Don’t put it in a leaf page component. - Script appears twice → the widget self-dedupes; harmless, but check you’re not importing the Layout twice.