ajouter un live chat à un site next.js

Un live chat dans Next.js, c’est une balise script dans le layout racine. Ce guide montre où elle va avec l’app router et le pages router, comment transmettre l’apparence du widget et l’utilisateur connecté sans casser l’ordre de chargement, quoi ajouter à une Content Security Policy, et comment vérifier le résultat dans un build de production.
la version courte
Avec l’app router, ajoutez la balise dans app/layout.tsx, à l’intérieur de <body> :
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<script src="https://cdn.spookat.com/s/YOUR_SITE_KEY.js" async></script>
</body>
</html>
);
}
Remplacez YOUR_SITE_KEY par la clé affichée sur la page d’installation du tableau de bord. C’est toute l’installation quand l’apparence vit dans le tableau de bord, ce qui est le cas par défaut. La suite de ce guide sert quand vous voulez plus de contrôle. Si vous partez de zéro, le guide de référence couvre la partie compte.
pourquoi le layout racine
Le layout racine englobe chaque route et reste monté pendant que l’utilisateur navigue. Le lanceur s’attache une seule fois à la page. La navigation côté client ne recharge pas la page, donc elle ne recharge pas non plus le lanceur, et un visiteur peut garder une conversation ouverte en cliquant partout.
Mettez la balise dans un composant de page et le chat n’existe que sur cette page. Mettez-la dans un composant rendu sur plusieurs pages et vous obtenez des doublons lors des rechargements complets (le lanceur ignore sa deuxième copie, mais c’est quand même une requête gaspillée).
avec next/script
next/script est la façon Next.js de charger des scripts tiers, et ça fonctionne bien ici :
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script src="https://cdn.spookat.com/s/YOUR_SITE_KEY.js" strategy="afterInteractive" />
</body>
</html>
);
}
afterInteractive charge le lanceur juste après l’hydratation. lazyOnload attend que le navigateur soit inactif, ce qui retarde un peu l’apparition du bouton sur les téléphones lents. Les deux conviennent pour un lanceur de 2 kb ; afterInteractive affiche le bouton plus tôt. N’utilisez pas beforeInteractive pour le lanceur : il entrerait en concurrence avec votre propre code pour rien.
définir l’apparence dans le code
L’apparence que vous publiez dans le tableau de bord voyage dans le lanceur. Si vous préférez la garder dans le dépôt, définissez window.Spookat avant que le lanceur ne s’exécute. La configuration de la page l’emporte sur celle du tableau de bord.
L’ordre de chargement compte : la configuration doit exister quand le lanceur s’exécute. Avec next/script, donnez beforeInteractive à la configuration (autorisé dans le layout racine) et afterInteractive au lanceur :
import Script from "next/script";
const spookat = { style: "clean", font: "system", accent: "#2F6FEB", side: "right" };
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script id="spookat-config" strategy="beforeInteractive">
{`window.Spookat = ${JSON.stringify(spookat)};`}
</Script>
<Script src="https://cdn.spookat.com/s/YOUR_SITE_KEY.js" strategy="afterInteractive" />
</body>
</html>
);
}
Les scripts beforeInteractive s’exécutent avant l’hydratation, les afterInteractive après, donc l’ordre est garanti. Les clés sont les mêmes que partout ailleurs : style (l’un des 8), font, accent, side, greeting, label, lang. La documentation les liste toutes.
le pages router
Avec le pages router, l’enveloppe partagée est pages/_app.tsx :
import type { AppProps } from "next/app";
import Script from "next/script";
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<Script src="https://cdn.spookat.com/s/YOUR_SITE_KEY.js" strategy="afterInteractive" />
</>
);
}
Pour une configuration, avec le pages router, les scripts beforeInteractive vont dans pages/_document.tsx. Mettez-y le script window.Spookat, dans <Head>, avec le même id et la même strategy que plus haut, et gardez le lanceur dans _app.tsx.
l’utilisateur connecté
À partir de pro, vous pouvez indiquer au chat qui est connecté. Votre équipe voit alors logged in as 42 sur la carte Slack, et les webhooks, l’export et le read_convo de votre agent transmettent l’identifiant. L’identifiant est signé avec un secret que seul votre serveur connaît, pour que personne ne puisse se faire passer pour quelqu’un d’autre.
Créez le secret d’identité sur la page d’installation du tableau de bord, placez-le dans une variable d’environnement et signez côté serveur. Avec l’app router, le layout racine est un composant serveur, ce qui rend la chose simple :
import { createHmac } from "node:crypto";
import Script from "next/script";
import { getSession } from "@/lib/auth"; // yours
const sign = (id: string) => createHmac("sha256", process.env.SPOOKAT_IDENTITY_SECRET!).update(id).digest("hex");
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const session = await getSession();
const user = session ? { id: String(session.userId), sig: sign(String(session.userId)) } : undefined;
return (
<html lang="en">
<body>
{children}
{user && (
<Script id="spookat-config" strategy="beforeInteractive">
{`window.Spookat = ${JSON.stringify({ user })};`}
</Script>
)}
<Script src="https://cdn.spookat.com/s/YOUR_SITE_KEY.js" strategy="afterInteractive" />
</body>
</html>
);
}
Ne calculez jamais la signature dans un composant client : le secret partirait vers le navigateur. Une signature absente ou fausse n’est pas une erreur ; le chat n’indique simplement pas qui c’est. Lire la session dans le layout racine fait rendre chaque page à chaque requête ; si votre layout est statique aujourd’hui, pesez cela avant de l’ajouter.
content security policy
Si votre application envoie un en-tête Content-Security-Policy, le widget a besoin de :
script-srcetstyle-src:https://cdn.spookat.comfont-src:https://cdn.spookat.com, si vous avez choisi une police hébergéeconnect-src:wss://api.spookat.com, pour la connexion du chat après un clicimg-src:data:, pour les logos et les photos de votre équipestyle-src: le lanceur injecte un petit bloc<style>dans sa propre shadow root, donc les styles inline doivent être autorisés pour lui
Sans cela, la console du navigateur indique précisément quelle directive a bloqué quoi. Ajoutez la source qu’elle nomme et rechargez.
vérifiez dans un build de production
Le mode développement ajoute ses propres scripts et le rechargement à chaud, alors vérifiez un vrai build :
next build && next start
Ouvrez http://localhost:3000 dans une fenêtre privée avec les DevTools ouverts. Le domaine compte : le chat ne se connecte que depuis le domaine enregistré et ses sous-domaines, donc sur localhost vous verrez le lanceur, mais le chat ne se connectera pas. Testez le chat lui-même sur votre site déployé.
Sur l’un comme sur l’autre :
- Network, « Disable cache », rechargez, filtrez
spookat: une requête, le lanceur depuiscdn.spookat.com. Deux, toutes deux depuiscdn.spookat.com, si votre code définit danswindow.Spookatun autre style que celui que vous avez publié. Rien versapi.spookat.com. - Application : aucun cookie de Spookat, et aucun stockage avant un premier message.
- Performance ou Lighthouse : le lanceur ne doit pas apparaître comme une tâche longue. Comparez quelques exécutions avec et sans lui ; la différence doit rester dans le bruit normal entre les exécutions.
- Cliquez sur le lanceur sur le site déployé : le panneau se charge, la connexion s’ouvre, votre message arrive dans Slack.
cibles de déploiement
Rien de tout cela ne dépend de l’endroit où tourne l’application. Vercel, un serveur Node, un conteneur ou output: "export" pour un site statique servent tous le même HTML, et la balise fonctionne de la même façon partout. Pour un export statique, l’utilisateur connecté doit venir d’un serveur que vous contrôlez, puisqu’il n’y a pas de rendu serveur pour le signer.
connectez les réponses
Une fois le chat sur le site, connectez Slack dans le tableau de bord et choisissez un canal. Le guide Slack vous accompagne. Si vous utilisez Claude Code, le guide Claude Code montre comment laisser l’agent faire tout ce qui précède.
applications multilingues
Si votre application a un segment [locale], indiquez au widget la langue de la page, pour que le panneau corresponde à la page plutôt qu’au navigateur du visiteur. Le widget parle 12 langues ; lang accepte des codes comme en, pl, de. Dans app/[locale]/layout.tsx :
<Script id="spookat-config" strategy="beforeInteractive">
{`window.Spookat = ${JSON.stringify({ lang: locale })};`}
</Script>
Une langue que le widget ne connaît pas se rabat sur celle du navigateur du visiteur, puis sur l’anglais. Votre équipe voit la langue sur la conversation dans tous les cas.
erreurs fréquentes
- La balise dans un composant client avec
"use client". Elle s’affiche, mais elle est hydratée avec le composant, et chaque remontage est une occasion de doublon. Gardez-la dans le layout. - Créer le script dans
useEffect. Plus de code pour le même résultat, et un chargement plus tardif qu’avecafterInteractive. type="module"sur la balise. Le lanceur est un script classique ; ne mettez pas de type.- Précharger le panneau. Un
<link rel="preload">pour le script du panneau ramène son téléchargement avant le clic pour chaque visiteur. Tout l’intérêt, c’est qu’il attende. - Tester le chat sur un déploiement de prévisualisation. Les URL de prévisualisation sont d’autres domaines. Le lanceur s’affiche, le chat ne se connecte pas. C’est normal.
ce que cela ajoute à votre bundle
Rien. Le lanceur vient de notre CDN comme fichier à part, il ne fait donc pas partie de votre bundle JavaScript, et les chiffres « First Load JS » qu’affiche next build restent exactement les mêmes. Il ne touche pas non plus à React : le bouton vit dans sa propre shadow root, donc votre CSS ne peut pas le casser et le sien ne peut pas déborder sur votre application. La seule chose que votre build doit connaître, c’est la balise.
checklist
- Une balise ou un
<Script>dans le layout racine (app/layout.tsxoupages/_app.tsx). - La configuration, s’il y en a une, s’exécute avant le lanceur.
- Les signatures utilisateur sont calculées uniquement côté serveur.
- La CSP autorise le CDN et la connexion du chat, si vous avez une CSP.
- Vérifié dans un build de production : une requête avant un clic (deux, toutes deux depuis
cdn.spookat.com, si votre code définit un style que vous n’avez pas publié), aucun cookie.
Cela coûte 4,99 € par mois et par site avec starter, et l’essai dure 14 jours sans carte.