Skip to content
GitHub
Get started →

Install on Angular

Angular apps ship a root index.html that’s served on every route. Angular routing uses the History API under the hood, which the widget auto-detects. One paste, done.

Steps

  1. Open src/index.html

  2. Paste the snippet before </body>:

    <body>
    <app-root></app-root>
    <script
    src="https://spelo.ai/spelo.js"
    data-site-id="YOUR_SITE_ID"
    async
    ></script>
    </body>
  3. Run ng serve and verify the orb appears.

As an Angular service (optional)

If you prefer to control widget lifecycle from Angular:

src/app/spelo.service.ts
import { Injectable, Inject, DOCUMENT } from '@angular/core'
@Injectable({ providedIn: 'root' })
export class SpeloService {
private loaded = false
constructor(@Inject(DOCUMENT) private doc: Document) {}
load(siteId: string): void {
if (this.loaded) return
if (this.doc.querySelector(`script[data-site-id="${siteId}"]`)) {
this.loaded = true
return
}
const s = this.doc.createElement('script')
s.src = 'https://spelo.ai/spelo.js'
s.setAttribute('data-site-id', siteId)
s.async = true
this.doc.body.appendChild(s)
this.loaded = true
}
}
src/app/app.component.ts
import { Component, OnInit } from '@angular/core'
import { SpeloService } from './spelo.service'
import { environment } from '../environments/environment'
@Component({ selector: 'app-root', template: '<router-outlet />' })
export class AppComponent implements OnInit {
constructor(private vk: SpeloService) {}
ngOnInit() {
this.vk.load(environment.speloSiteId)
}
}

Add speloSiteId to your src/environments/environment.ts files so each environment can have a different site.

Angular Router

The widget listens for popstate and patches pushState/replaceState so SPA navigations are detected. Angular’s Router uses these underneath — nothing extra to configure.

Angular Universal (SSR)

On the server side, the widget <script> is emitted into the rendered HTML but does not execute (it’s a browser-only script). In the browser, it runs on hydration. No SSR issues.

If you want to suppress the script on the server (for strictness), wrap the load call in isPlatformBrowser:

import { Inject, PLATFORM_ID } from '@angular/core'
import { isPlatformBrowser } from '@angular/common'
constructor(@Inject(PLATFORM_ID) private platformId: object, private vk: SpeloService) {}
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
this.vk.load(environment.speloSiteId)
}
}

Zone.js

The widget runs outside Angular’s zone (it doesn’t use any Angular APIs). That means it won’t trigger change detection. You don’t need ngZone.runOutsideAngular — it’s already outside.

CSP

Angular doesn’t add a CSP by default. If you’ve configured one (via your reverse proxy or meta tag), allow:

script-src 'self' https://spelo.ai;
connect-src 'self' https://api.spelo.ai https://api.openai.com wss://*;
media-src 'self' blob:;

Angular’s optional strict CSP (no eval, no inline) works fine with the widget — it doesn’t use eval or inline script.

Verify

  1. ng serve
  2. Hit http://localhost:4200
  3. Orb at the bottom; click → allow mic → speak

Troubleshooting

  • Widget appears, but route changes remove it → shouldn’t happen. If it does, ensure the <script> tag is in src/index.html (outside <app-root>), not inside a component template.
  • Mic permission denied → Angular is served on http://localhost by default, which browsers treat as secure for mic access. In production, ensure you’re on https://.