widget/Framework Integration
Framework Integration
The Savanto widgets work with any JavaScript framework. This guide covers integration patterns for popular frameworks.
React
Basic Integration
Load the widget in a component:
import { useEffect } from 'react';
export function SavantoChat() {
useEffect(() => {
// Load the widget script
const script = document.createElement('script');
script.src = 'https://cdn.savanto.ai/chat-loader.js';
script.dataset.publishableKey = 'if_pk_xxx';
script.async = true;
document.body.appendChild(script);
return () => {
// Cleanup on unmount
window.SavantoChatbot?.destroy();
document.body.removeChild(script);
};
}, []);
return null;
}
Custom React Hook
Create a reusable hook:
// hooks/useSavantoChat.ts
import { useEffect, useCallback } from 'react';
interface UseSavantoChatOptions {
publishableKey: string;
title?: string;
greeting?: string;
theme?: Record<string, any>;
}
export function useSavantoChat(options: UseSavantoChatOptions) {
useEffect(() => {
// Set config before loading script
window.savantoConfig = {
publishableKey: options.publishableKey,
title: options.title,
greeting: options.greeting,
theme: options.theme,
};
const script = document.createElement('script');
script.src = 'https://cdn.savanto.ai/chat-loader.js';
script.async = true;
document.body.appendChild(script);
return () => {
window.SavantoChatbot?.destroy();
document.body.removeChild(script);
};
}, [options.publishableKey]);
const open = useCallback(() => {
window.SavantoChatbot?.open();
}, []);
const close = useCallback(() => {
window.SavantoChatbot?.close();
}, []);
const toggle = useCallback(() => {
window.SavantoChatbot?.toggle();
}, []);
return { open, close, toggle };
}
Usage:
function App() {
const { open } = useSavantoChat({
publishableKey: 'if_pk_xxx',
title: 'Help Center',
});
return (
<button onClick={open}>
Need help?
</button>
);
}
TypeScript Declarations
Add type declarations for the global API:
// types/savanto.d.ts
interface SavantoChatbotAPI {
open(): void;
close(): void;
toggle(): void;
isOpen(): boolean;
getConfig(): Record<string, any>;
configure(config: Record<string, any>): void;
setTheme(theme: Record<string, any>): void;
setCustomCSS(css: string): void;
destroy(): void;
grantAnalyticsConsent(): void;
revokeAnalyticsConsent(): void;
getAnalyticsConsentStatus(): 'granted' | 'denied' | 'pending';
}
interface SavantoSearchAPI {
open(): void;
close(): void;
toggle(): void;
isOpen(): boolean;
search(query: string): void;
getConfig(): Record<string, any>;
configure(config: Record<string, any>): void;
setTheme(theme: Record<string, any>): void;
setCustomCSS(css: string): void;
destroy(): void;
isInitialized(): boolean;
version: string;
}
declare global {
interface Window {
savantoConfig?: Record<string, any>;
savantoSearchConfig?: Record<string, any>;
SavantoChatbot?: SavantoChatbotAPI;
SavantoSearch?: SavantoSearchAPI;
}
}
export {};
Next.js
App Router (Next.js 13+)
Create a client component:
// components/SavantoWidgets.tsx
'use client';
import { useEffect } from 'react';
interface Props {
publishableKey: string;
chatEnabled?: boolean;
searchEnabled?: boolean;
}
export function SavantoWidgets({
publishableKey,
chatEnabled = true,
searchEnabled = true
}: Props) {
useEffect(() => {
const scripts: HTMLScriptElement[] = [];
if (chatEnabled) {
window.savantoConfig = { publishableKey };
const chatScript = document.createElement('script');
chatScript.src = 'https://cdn.savanto.ai/chat-loader.js';
chatScript.async = true;
document.body.appendChild(chatScript);
scripts.push(chatScript);
}
if (searchEnabled) {
window.savantoSearchConfig = { publishableKey };
const searchScript = document.createElement('script');
searchScript.src = 'https://cdn.savanto.ai/search-loader.js';
searchScript.async = true;
document.body.appendChild(searchScript);
scripts.push(searchScript);
}
return () => {
window.SavantoChatbot?.destroy();
window.SavantoSearch?.destroy();
scripts.forEach(s => s.remove());
};
}, [publishableKey, chatEnabled, searchEnabled]);
return null;
}
Add to your layout:
// app/layout.tsx
import { SavantoWidgets } from '@/components/SavantoWidgets';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<SavantoWidgets publishableKey="if_pk_xxx" />
</body>
</html>
);
}
Pages Router
Use next/script:
// pages/_app.tsx
import Script from 'next/script';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Script id="savanto-config" strategy="beforeInteractive">
{`window.savantoConfig = { publishableKey: 'if_pk_xxx' };`}
</Script>
<Script
src="https://cdn.savanto.ai/chat-loader.js"
strategy="afterInteractive"
/>
<Component {...pageProps} />
</>
);
}
Environment Variables
Use environment variables for the publishable key:
# .env.local
NEXT_PUBLIC_SAVANTO_KEY=if_pk_xxx
<SavantoWidgets publishableKey={process.env.NEXT_PUBLIC_SAVANTO_KEY!} />
Dynamic Import (Code Splitting)
Load the widget component dynamically:
import dynamic from 'next/dynamic';
const SavantoWidgets = dynamic(
() => import('@/components/SavantoWidgets').then(mod => mod.SavantoWidgets),
{ ssr: false }
);
export default function Page() {
return (
<>
<main>Your content</main>
<SavantoWidgets publishableKey="if_pk_xxx" />
</>
);
}
Vue.js
Vue 3 Composition API
<!-- components/SavantoChat.vue -->
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
interface Props {
publishableKey: string;
title?: string;
}
const props = defineProps<Props>();
onMounted(() => {
window.savantoConfig = {
publishableKey: props.publishableKey,
title: props.title,
};
const script = document.createElement('script');
script.src = 'https://cdn.savanto.ai/chat-loader.js';
script.async = true;
document.body.appendChild(script);
});
onUnmounted(() => {
window.SavantoChatbot?.destroy();
});
</script>
<template>
<!-- Renders nothing, widget is injected into body -->
</template>
Vue 3 Composable
// composables/useSavanto.ts
import { onMounted, onUnmounted } from 'vue';
export function useSavantoChat(config: {
publishableKey: string;
title?: string;
greeting?: string;
}) {
onMounted(() => {
window.savantoConfig = config;
const script = document.createElement('script');
script.src = 'https://cdn.savanto.ai/chat-loader.js';
script.async = true;
document.body.appendChild(script);
});
onUnmounted(() => {
window.SavantoChatbot?.destroy();
});
return {
open: () => window.SavantoChatbot?.open(),
close: () => window.SavantoChatbot?.close(),
toggle: () => window.SavantoChatbot?.toggle(),
};
}
Nuxt 3
Create a plugin:
// plugins/savanto.client.ts
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
window.savantoConfig = {
publishableKey: config.public.savantoKey,
};
useHead({
script: [
{
src: 'https://cdn.savanto.ai/chat-loader.js',
async: true,
},
],
});
});
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
savantoKey: process.env.SAVANTO_KEY,
},
},
});
Angular
Service
// services/savanto.service.ts
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class SavantoService {
private loaded = false;
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
init(config: { publishableKey: string; title?: string }) {
if (!isPlatformBrowser(this.platformId) || this.loaded) return;
(window as any).savantoConfig = config;
const script = document.createElement('script');
script.src = 'https://cdn.savanto.ai/chat-loader.js';
script.async = true;
document.body.appendChild(script);
this.loaded = true;
}
open() {
(window as any).SavantoChatbot?.open();
}
close() {
(window as any).SavantoChatbot?.close();
}
destroy() {
(window as any).SavantoChatbot?.destroy();
this.loaded = false;
}
}
Component
// app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { SavantoService } from './services/savanto.service';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
template: `<router-outlet></router-outlet>`,
})
export class AppComponent implements OnInit, OnDestroy {
constructor(private savanto: SavantoService) {}
ngOnInit() {
this.savanto.init({
publishableKey: environment.savantoKey,
});
}
ngOnDestroy() {
this.savanto.destroy();
}
}
Svelte
Component
<!-- SavantoChat.svelte -->
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
export let publishableKey: string;
export let title: string | undefined = undefined;
onMount(() => {
window.savantoConfig = {
publishableKey,
title,
};
const script = document.createElement('script');
script.src = 'https://cdn.savanto.ai/chat-loader.js';
script.async = true;
document.body.appendChild(script);
return () => {
window.SavantoChatbot?.destroy();
script.remove();
};
});
</script>
SvelteKit
<!-- +layout.svelte -->
<script lang="ts">
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import { PUBLIC_SAVANTO_KEY } from '$env/static/public';
onMount(() => {
if (!browser) return;
window.savantoConfig = {
publishableKey: PUBLIC_SAVANTO_KEY,
};
const script = document.createElement('script');
script.src = 'https://cdn.savanto.ai/chat-loader.js';
script.async = true;
document.body.appendChild(script);
});
</script>
<slot />
Astro
Component
---
// components/SavantoChat.astro
interface Props {
publishableKey: string;
}
const { publishableKey } = Astro.props;
---
<script define:vars={{ publishableKey }}>
window.savantoConfig = {
publishableKey,
};
</script>
<script src="https://cdn.savanto.ai/chat-loader.js" async></script>
Usage in layout:
---
// layouts/Layout.astro
import SavantoChat from '../components/SavantoChat.astro';
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Site</title>
</head>
<body>
<slot />
<SavantoChat publishableKey="if_pk_xxx" />
</body>
</html>
Common Patterns
Conditional Loading
Only load on certain pages:
// React example
function ProductPage() {
const [showChat, setShowChat] = useState(false);
useEffect(() => {
// Load chat only on product pages
setShowChat(true);
}, []);
return (
<>
<ProductContent />
{showChat && <SavantoChat publishableKey="if_pk_xxx" />}
</>
);
}
User Authentication
Pass user JWT for authenticated features:
function App() {
const { user, getToken } = useAuth();
useEffect(() => {
if (user) {
const token = await getToken();
window.SavantoChatbot?.configure({
userJwt: token,
});
}
}, [user]);
return <SavantoChat publishableKey="if_pk_xxx" />;
}
Custom Trigger Buttons
function Header() {
return (
<nav>
<button onClick={() => window.SavantoSearch?.open()}>
<SearchIcon /> Search (⌘K)
</button>
<button onClick={() => window.SavantoChatbot?.open()}>
<ChatIcon /> Help
</button>
</nav>
);
}
Troubleshooting
Widget not loading in SPA
Ensure you're only loading the script once:
const loadedRef = useRef(false);
useEffect(() => {
if (loadedRef.current) return;
loadedRef.current = true;
// Load script...
}, []);
Hydration mismatch
Use client-only rendering:
// Next.js
const SavantoChat = dynamic(() => import('./SavantoChat'), {
ssr: false,
});
Widget persists after navigation
Call destroy() on unmount:
useEffect(() => {
// Load widget...
return () => {
window.SavantoChatbot?.destroy();
};
}, []);
Next Steps
- Chat Widget — Full configuration reference
- Search Widget — Search widget options
- Widget Styling — Theming and customization