- Docs
- Install
Install in Vue & Nuxt
Add the TalkingDot messenger to a Vue 3 app built with Vite, or to a Nuxt 3 site. Load it once at the app root, identify signed-in users, and let it follow router changes automatically.
Before you start: copy your workspace key from Settings → Install. It's the 16-character data-key value. The key is public, so it can live in VITE_TALKINGDOT_KEY or Nuxt's public runtime config. Your workspace secret (Settings → Security) must stay on the server.
Vue 3 (Vite): quickest option
Paste the snippet into index.html in your project root, just before </body>. It loads once per page load, outside Vue, and nothing else is needed.
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script src="https://talkingdot.com/widget.js" data-key="YOUR_WORKSPACE_KEY" async></script>
</body>Vue 3 (Vite): load from main.ts
To keep the key in an environment variable, load the script from code instead. This helper adds widget.js only once, and queues calls made before the script has loaded:
type TalkingDotFn = ((...args: any[]) => any) & { q?: unknown[][] };
declare global {
interface Window {
TalkingDot?: TalkingDotFn;
TalkingDotSettings?: Record<string, unknown>;
}
}
function api(): TalkingDotFn {
if (!window.TalkingDot) {
const q: unknown[][] = [];
window.TalkingDot = Object.assign((...args: unknown[]) => { q.push(args); }, { q });
}
return window.TalkingDot;
}
export function talkingdot(...args: unknown[]): unknown {
return typeof window === 'undefined' ? undefined : api()(...args);
}
export function loadTalkingDot(key: string): void {
if (typeof window === 'undefined' || document.getElementById('talkingdot-js')) return;
api();
const s = document.createElement('script');
s.id = 'talkingdot-js';
s.async = true;
s.src = 'https://talkingdot.com/widget.js';
s.setAttribute('data-key', key);
document.head.appendChild(s);
}import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { loadTalkingDot } from './talkingdot';
createApp(App).use(router).mount('#app');
loadTalkingDot(import.meta.env.VITE_TALKINGDOT_KEY);You can also call loadTalkingDot() from onMounted in App.vue. Either way, call it from the root once, not from individual route components.
Nuxt 3
Add the key to your public runtime config. You can set it with the NUXT_PUBLIC_TALKINGDOT_KEY environment variable:
export default defineNuxtConfig({
runtimeConfig: {
talkingdotSecret: '', // server only (NUXT_TALKINGDOT_SECRET), used for user_hash
public: { talkingdotKey: '' }, // NUXT_PUBLIC_TALKINGDOT_KEY
},
});Option A: useHead in app.vue
<script setup lang="ts">
const config = useRuntimeConfig();
useHead({
script: [
{ src: 'https://talkingdot.com/widget.js', 'data-key': config.public.talkingdotKey, async: true },
],
});
</script>
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>Because app.vue wraps every page, the tag is rendered once and stays loaded as visitors navigate.
Option B: a client-only plugin
A .client.ts plugin runs only in the browser. It also gives you $talkingdot() everywhere, with calls queued until the script loads:
export default defineNuxtPlugin(() => {
const key = useRuntimeConfig().public.talkingdotKey as string;
const w = window as any;
w.TalkingDot = w.TalkingDot || function () { (w.TalkingDot.q = w.TalkingDot.q || []).push(arguments); };
if (!document.getElementById('talkingdot-js')) {
const s = document.createElement('script');
s.id = 'talkingdot-js';
s.async = true;
s.src = 'https://talkingdot.com/widget.js';
s.setAttribute('data-key', key);
document.head.appendChild(s);
}
return { provide: { talkingdot: (...args: unknown[]) => w.TalkingDot(...args) } };
});Use either Option A or Option B, not both.
Identify signed-in users
Watch your auth state. Call update when a user is known, and shutdown after a real sign-out so the next person on that browser starts a fresh, anonymous session. The user_hash must come from your server. See Identity verification.
<script setup lang="ts">
import { watch } from 'vue';
import { useAuthStore } from '@/stores/auth'; // your own store (Pinia, etc.)
import { talkingdot } from '@/talkingdot'; // Nuxt: const { $talkingdot } = useNuxtApp()
const auth = useAuthStore();
const FLAG = 'talkingdot_identified';
watch(
() => (auth.ready ? auth.user : undefined), // undefined while the auth check is loading
(user) => {
if (user === undefined) return;
if (user) {
talkingdot('update', { user_id: String(user.id), email: user.email, name: user.name, user_hash: user.talkingdotHash });
localStorage.setItem(FLAG, '1');
} else if (localStorage.getItem(FLAG)) {
localStorage.removeItem(FLAG);
talkingdot('shutdown');
}
},
{ immediate: true },
);
</script>In Nuxt, compute the hash in a server route, so talkingdotSecret never leaves the server, and return it with the current user:
import { createHmac } from 'node:crypto';
export default defineEventHandler(async (event) => {
const user = await getUserFromSession(event); // your own auth
if (!user) return null;
const hash = createHmac('sha256', useRuntimeConfig(event).talkingdotSecret)
.update(String(user.id))
.digest('hex');
return { id: user.id, email: user.email, name: user.name, talkingdotHash: hash };
});Router changes
Nothing to add. The messenger watches the History API and hash changes, so every Vue Router and Nuxt navigation is recorded as a page view. Page rules and proactive messages are checked again on each route. Don't add router hooks that reload the script or call shutdown on navigation.
Verify it works
- Open your app in a private window. The launcher bubble appears in the bottom-right corner, or on whichever side you chose.
- Navigate between a few routes, then send a test message. In the Inbox, the contact sidebar shows the pages you visited.
- Sign in, then sign out. Your user's details appear on the conversation. After signing out, the messenger starts an empty, anonymous session.
Troubleshooting
TalkingDot: missing or invalid data-key: the key variable is empty. Vite only exposesVITE_variables, and Nuxt readsNUXT_PUBLIC_TALKINGDOT_KEYat runtime. Restart the dev server after changing.env.- Hydration warnings or
window is not defined: browser-only code ran on the server. Keep it inonMounted, a.client.tsplugin, or behindimport.meta.client. - Nothing on localhost or preview deploys: if you set a domain allow-list in Settings → Security, add
localhostand your preview domains. widget.jsdownloads twice: the snippet is in bothindex.htmland your code, or you used both Nuxt options. Keep one. The second copy is ignored, but it still costs a download.
Next steps
- JavaScript API:
open,showNewMessage,trackand event listeners. - Identity verification: how
user_hashis computed and how to require it. - Chatbots and Office hours: handle conversations when nobody's online.