# ajouter un live chat à un site next.js

Ajoutez le chat de Spookat à une application Next.js, avec l’app router ou le pages router, son apparence et éventuellement l’utilisateur connecté, et vérifiez qu’aucune requête ne part vers notre API avant un clic.

2026-11-04 · Kuba

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>` :

```tsx
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](/fr/blog/add-live-chat-to-your-website/) 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 :

```tsx
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 :

```tsx
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](/fr/docs/) les liste toutes.

## le pages router

Avec le pages router, l’enveloppe partagée est `pages/_app.tsx` :

```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 :

```tsx
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-src` et `style-src` : `https://cdn.spookat.com`
- `font-src` : `https://cdn.spookat.com`, si vous avez choisi une police hébergée
- `connect-src` : `wss://api.spookat.com`, pour la connexion du chat après un clic
- `img-src` : `data:`, pour les logos et les photos de votre équipe
- `style-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 :

```sh
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 :

1. **Network**, « Disable cache », rechargez, filtrez `spookat` : une requête, le lanceur depuis `cdn.spookat.com`. Deux, toutes deux depuis `cdn.spookat.com`, si votre code définit dans `window.Spookat` un autre style que celui que vous avez publié. Rien vers `api.spookat.com`.
2. **Application** : aucun cookie de Spookat, et aucun stockage avant un premier message.
3. **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.
4. **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](/fr/blog/answer-website-chat-from-slack/) vous accompagne. Si vous utilisez Claude Code, le [guide Claude Code](/fr/blog/claude-code-live-chat/) 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` :

```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’avec `afterInteractive`.
- **`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.tsx` ou `pages/_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.

---
source: https://spookat.com/fr/blog/live-chat-nextjs/
