Skip to content
GitHub
Get started →

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

  1. Open your layout — typically src/layouts/Layout.astro or similar.

  2. Paste the snippet before </body>:

    src/layouts/Layout.astro
    <html lang="en">
    <head>
    <slot name="head" />
    </head>
    <body>
    <slot />
    <script
    is:inline
    src="https://spelo.ai/spelo.js"
    data-site-id="YOUR_SITE_ID"
    async
    ></script>
    </body>
    </html>

    The is:inline directive tells Astro to emit the script as-is (not bundle it).

  3. 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:

src/layouts/Layout.astro
---
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_:

.env
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:

astro.config.mjs
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/partytowndo not route the Spelo script through Partytown. Partytown moves scripts off the main thread into a web worker, and the widget needs access to navigator.mediaDevices, WebRTC, and the DOM. Exclude spelo.js from 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

  1. npm run dev
  2. Hit http://localhost:4321
  3. 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.