# aggiungi una live chat a un sito next.js

Aggiungi la chat di Spookat a un’app Next.js, con app router o pages router, con il suo aspetto e, se vuoi, l’utente connesso, e verifica che non parta nessuna richiesta alla nostra API prima di un clic.

2026-11-04 · Kuba

La live chat in Next.js è un tag script nel layout principale. Questa guida mostra dove va con l’app router e con il pages router, come passare l’aspetto del widget e l’utente connesso senza rompere l’ordine di caricamento, cosa aggiungere a una Content Security Policy e come controllare il risultato in una build di produzione.

## la versione breve

Con l’app router, aggiungi il tag in `app/layout.tsx`, dentro `<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>
  );
}
```

Sostituisci `YOUR_SITE_KEY` con la chiave della pagina di installazione nella dashboard. È tutta l’installazione quando l’aspetto sta nella dashboard, che è il caso predefinito. Il resto della guida serve quando vuoi più controllo. Se parti da zero, la [guida principale](/it/blog/add-live-chat-to-your-website/) copre la parte dell’account.

## perché il layout principale

Il layout principale avvolge ogni route e resta montato mentre l’utente naviga. Il launcher si aggancia alla pagina una sola volta. La navigazione lato client non ricarica la pagina, quindi non ricarica nemmeno il launcher, e un visitatore può tenere aperta una conversazione mentre clicca in giro.

Metti il tag in un componente di pagina e la chat esiste solo su quella pagina. Mettilo in un componente che viene renderizzato su più pagine e avrai duplicati nei ricaricamenti completi (il launcher ignora la propria seconda copia, ma resta una richiesta sprecata).

## con next/script

`next/script` è il modo di Next.js per caricare script di terze parti, e qui funziona bene:

```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` carica il launcher subito dopo l’idratazione. `lazyOnload` aspetta che il browser sia inattivo, e sui telefoni lenti fa comparire il pulsante un po’ più tardi. Vanno bene entrambi per un launcher da 2 kb; `afterInteractive` mostra il pulsante prima. Non usare `beforeInteractive` per il launcher: entrerebbe in competizione con il tuo codice per niente.

## impostare l’aspetto nel codice

L’aspetto che pubblichi nella dashboard viaggia dentro il launcher. Se preferisci tenerlo nel repository, imposta `window.Spookat` prima che il launcher venga eseguito. La configurazione della pagina vince su quella della dashboard.

L’ordine di caricamento conta: la configurazione deve esistere quando il launcher parte. Con `next/script`, dai `beforeInteractive` alla configurazione (consentito nel layout principale) e `afterInteractive` al launcher:

```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>
  );
}
```

Gli script `beforeInteractive` girano prima dell’idratazione, quelli `afterInteractive` dopo, quindi l’ordine è garantito. Le chiavi sono le stesse di ovunque: `style` (uno dei 8), `font`, `accent`, `side`, `greeting`, `label`, `lang`. La [documentazione](/it/docs/) le elenca tutte.

## il pages router

Con il pages router, il wrapper condiviso è `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" />
    </>
  );
}
```

Per una configurazione, nel pages router gli script `beforeInteractive` vanno in `pages/_document.tsx`. Metti lì lo script di `window.Spookat`, dentro `<Head>`, con lo stesso `id` e la stessa `strategy` di sopra, e lascia il launcher in `_app.tsx`.

## l’utente connesso

Da pro in su, puoi dire alla chat chi ha effettuato l’accesso. Il tuo team vedrà `logged in as 42` sulla scheda di Slack, e i webhook, l’export e il `read_convo` del tuo agente porteranno l’id. L’id viene firmato con un segreto che conosce solo il tuo server, così nessuno può fingersi qualcun altro.

Crea il segreto di identità nella pagina di installazione della dashboard, mettilo in una variabile d’ambiente e firma sul server. Con l’app router il layout principale è un componente server, il che rende tutto semplice:

```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>
  );
}
```

Non calcolare mai la firma in un componente client: il segreto finirebbe nel browser. Una firma mancante o sbagliata non è un errore; semplicemente la chat non dice chi è. Leggere la sessione nel layout principale fa renderizzare ogni pagina a ogni richiesta, quindi se oggi il tuo layout è statico, valutalo prima di aggiungerlo.

## content security policy

Se la tua app invia un header `Content-Security-Policy`, il widget ha bisogno di:

- `script-src` e `style-src`: `https://cdn.spookat.com`
- `font-src`: `https://cdn.spookat.com`, se hai scelto un font ospitato
- `connect-src`: `wss://api.spookat.com`, per la connessione della chat dopo un clic
- `img-src`: `data:`, per i loghi e le foto del team
- `style-src`: il launcher inietta un piccolo blocco `<style>` nella propria shadow root, quindi gli stili inline devono essere consentiti per lui

Senza queste voci, la console del browser dice esattamente quale direttiva ha bloccato cosa. Aggiungi la sorgente che indica e ricarica.

## controlla in una build di produzione

La modalità di sviluppo aggiunge i suoi script e l’hot reload, quindi controlla una build vera:

```sh
next build && next start
```

Apri `http://localhost:3000` in una finestra privata con i DevTools aperti. Il dominio conta: la chat si connette solo dal dominio che hai registrato e dai suoi sottodomini, quindi su localhost vedrai il launcher, ma la chat non si connetterà. La chat vera e propria provala sul sito pubblicato.

Su entrambi:

1. **Network**, "Disable cache", ricarica, filtra per `spookat`: una richiesta, il launcher da `cdn.spookat.com`. Due, entrambe da `cdn.spookat.com`, se il tuo codice imposta in `window.Spookat` uno stile diverso da quello che hai pubblicato. Niente verso `api.spookat.com`.
2. **Application**: nessun cookie di Spookat e nessuno storage fino al primo messaggio.
3. **Performance o Lighthouse**: il launcher non dovrebbe comparire come un long task. Confronta qualche esecuzione con e senza; la differenza dovrebbe restare nel normale rumore tra un’esecuzione e l’altra.
4. **Clicca sul launcher** sul sito pubblicato: il pannello si carica, la connessione si apre, il tuo messaggio arriva in Slack.

## destinazioni di deploy

Niente di tutto questo dipende da dove gira l’app. Vercel, un server Node, un container o `output: "export"` per un sito statico servono tutti lo stesso HTML, e il tag funziona allo stesso modo ovunque. Con un export statico, l’utente connesso deve arrivare da un server che controlli, perché non c’è un rendering lato server che possa firmarlo.

## collega le risposte

Quando la chat è sul sito, collega Slack nella dashboard e scegli un canale. La [guida Slack](/it/blog/answer-website-chat-from-slack/) ti accompagna. Se usi Claude Code, la [guida Claude Code](/it/blog/claude-code-live-chat/) mostra come far fare tutto questo all’agente.

## app multilingue

Se la tua app ha un segmento `[locale]`, di’ al widget in che lingua è la pagina, così il pannello segue la pagina e non il browser del visitatore. Il widget parla 12 lingue; `lang` accetta codici come `en`, `pl`, `de`. In `app/[locale]/layout.tsx`:

```tsx
<Script id="spookat-config" strategy="beforeInteractive">
  {`window.Spookat = ${JSON.stringify({ lang: locale })};`}
</Script>
```

Una lingua che il widget non ha ripiega su quella del browser del visitatore, poi sull’inglese. Il tuo team vede comunque la lingua sulla chat.

## errori comuni

- **Il tag in un componente client con `"use client"`.** Viene renderizzato, ma si idrata insieme al componente, e ogni rimontaggio è un’altra occasione per un duplicato. Tienilo nel layout.
- **Creare lo script in `useEffect`.** Più codice per lo stesso risultato, e si carica più tardi di `afterInteractive`.
- **`type="module"` sul tag.** Il launcher è uno script classico; non mettere il tipo.
- **Precaricare il pannello.** Un `<link rel="preload">` per lo script del pannello sposta il suo download prima del clic per ogni visitatore. Il punto è proprio che aspetti.
- **Provare la chat su un deploy di anteprima.** Gli URL di anteprima sono altri domini. Il launcher compare, la chat non si connette. È normale.

## cosa aggiunge al tuo bundle

Niente. Il launcher arriva dalla nostra CDN come file a sé, quindi non fa parte del tuo bundle JavaScript, e i numeri di "First Load JS" che stampa `next build` restano identici. Non tocca nemmeno React: il pulsante vive nella sua shadow root, quindi il tuo CSS non può romperlo e il suo CSS non può finire nella tua app. L’unica cosa che la tua build deve sapere è il tag.

## checklist

- Un tag o uno `<Script>` nel layout principale (`app/layout.tsx` o `pages/_app.tsx`).
- La configurazione, se c’è, viene eseguita prima del launcher.
- Le firme utente vengono calcolate solo sul server.
- La CSP consente la CDN e la connessione della chat, se hai una CSP.
- Controllato in una build di produzione: una richiesta prima di un clic (due, entrambe da `cdn.spookat.com`, se il tuo codice imposta uno stile che non hai pubblicato), nessun cookie.

Costa 4,99 € al mese per sito con starter, e la prova dura 14 giorni senza carta.

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