# dodaj live chat do strony w next.js

Wepnij czat Spookat w appkę Next.js, w app routerze albo pages routerze, z wyglądem i opcjonalnie zalogowanym userem, i sprawdź, że przed kliknięciem nie leci ani jeden request do naszego API.

2026-11-04 · Kuba

Live chat w Next.js to jeden tag script w głównym layoucie. Z tego poradnika dowiesz się, gdzie go dać w app routerze i w pages routerze, jak przekazać wygląd widżetu i zalogowanego usera bez psucia kolejności ładowania, co dopisać do Content Security Policy i jak sprawdzić efekt na produkcyjnym buildzie.

## wersja krótka

W app routerze dodaj tag do `app/layout.tsx`, wewnątrz `<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>
  );
}
```

Zamiast `YOUR_SITE_KEY` wstaw klucz ze strony instalacji w dashboardzie. To cała instalacja, jeśli wygląd siedzi w dashboardzie, a tak jest domyślnie. Reszta poradnika jest na wypadek, gdy chcesz mieć więcej kontroli. Jeśli zaczynasz od zera, [główny poradnik](/pl/blog/add-live-chat-to-your-website/) ogarnia stronę kontową.

## czemu główny layout

Główny layout owija każdą ścieżkę i zostaje zamontowany, kiedy user nawiguje. Launcher podpina się do strony raz. Nawigacja po stronie klienta nie przeładowuje strony, więc nie przeładowuje też launchera, a odwiedzający może mieć otwartą rozmowę, klikając dalej.

Wrzuć tag do komponentu strony, a czat będzie istniał tylko na tej stronie. Wrzuć go do komponentu, który renderuje się na kilku stronach, a przy twardych przeładowaniach dostaniesz duplikaty (launcher ignoruje swoją drugą kopię, ale to i tak zmarnowany request).

## z next/script

`next/script` to next.js-owy sposób ładowania skryptów z zewnątrz i tutaj działa bez problemu:

```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` ładuje launcher zaraz po hydracji. `lazyOnload` czeka, aż przeglądarka będzie bezczynna, co na wolnych telefonach trochę opóźnia pojawienie się przycisku. Przy 2 kb launchera oba są okej; `afterInteractive` szybciej pokazuje przycisk. Nie używaj `beforeInteractive` do launchera: konkurowałby z twoim kodem za nic.

## wygląd w kodzie

Wygląd opublikowany w dashboardzie jedzie w środku launchera. Jeśli wolisz trzymać go w repo, ustaw `window.Spookat`, zanim launcher się odpali. Konfiguracja ze strony wygrywa z dashboardem.

Kolejność ładowania ma znaczenie: konfiguracja musi istnieć, kiedy launcher się wykonuje. Z `next/script` daj konfiguracji `beforeInteractive` (dozwolone w głównym layoucie), a launcherowi `afterInteractive`:

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

Skrypty `beforeInteractive` odpalają się przed hydracją, `afterInteractive` po niej, więc kolejność jest pewna. Klucze są te same co wszędzie: `style` (jeden z 8), `font`, `accent`, `side`, `greeting`, `label`, `lang`. [Dokumentacja](/pl/docs/) wymienia wszystkie.

## pages router

W pages routerze wspólny wrapper to `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" />
    </>
  );
}
```

W pages routerze skrypty `beforeInteractive` należą do `pages/_document.tsx`. Daj tam skrypt z `window.Spookat`, wewnątrz `<Head>`, z tym samym `id` i `strategy` co wyżej, a launcher zostaw w `_app.tsx`.

## zalogowany user

Na pro i wyżej możesz powiedzieć czatowi, kto jest zalogowany. Twój zespół widzi wtedy `logged in as 42` na karcie na Slacku, a webhooki, eksport i `read_convo` twojego agenta niosą to id. Id jest podpisane sekretem, który zna tylko twój serwer, więc nikt nie podszyje się pod kogoś innego.

Zrób sekret tożsamości na stronie instalacji w dashboardzie, wrzuć go do zmiennej środowiskowej i podpisuj na serwerze. W app routerze główny layout to server component, co wszystko upraszcza:

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

Nigdy nie licz podpisu w client componencie: sekret poleciałby do przeglądarki. Brak podpisu albo zły podpis to nie błąd; czat po prostu nie mówi, kto pisze. Czytanie sesji w głównym layoucie sprawia, że każda strona renderuje się per request, więc jeśli twój layout jest dziś statyczny, przemyśl to, zanim to dodasz.

## content security policy

Jeśli twoja appka wysyła nagłówek `Content-Security-Policy`, widżet potrzebuje:

- `script-src` i `style-src`: `https://cdn.spookat.com`
- `font-src`: `https://cdn.spookat.com`, jeśli wybrałeś font hostowany u nas
- `connect-src`: `wss://api.spookat.com`, na połączenie czatu po kliknięciu
- `img-src`: `data:`, na logo i zdjęcia ludzi z zespołu
- `style-src`: launcher wstrzykuje jeden mały blok `<style>` do własnego shadow roota, więc inline style muszą być dla niego dozwolone

Bez tego konsola przeglądarki powie dokładnie, która dyrektywa co zablokowała. Dodaj źródło, które wskazuje, i przeładuj.

## sprawdź na produkcyjnym buildzie

Tryb dev dokłada swoje skrypty i hot reload, więc sprawdzaj prawdziwy build:

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

Otwórz `http://localhost:3000` w oknie prywatnym z otwartym DevTools. Domena ma znaczenie: czat łączy się tylko z zarejestrowanej domeny i jej subdomen, więc na localhoście zobaczysz launcher, ale czat się nie połączy. Sam czat testuj na wdrożonej stronie.

Na jednym i drugim:

1. **Network**, „Disable cache”, przeładuj, filtr `spookat`: jeden request, launcher z `cdn.spookat.com`. Dwa, oba z `cdn.spookat.com`, jeśli twój kod ustawia w `window.Spookat` inny styl niż ten, który opublikowałeś. Nic do `api.spookat.com`.
2. **Application**: żadnych cookies od Spookat i żadnego storage do pierwszej wiadomości.
3. **Performance albo Lighthouse**: launcher nie powinien pojawić się jako long task. Porównaj kilka przebiegów z nim i bez niego; różnica powinna mieścić się w zwykłym szumie między przebiegami.
4. **Kliknij launcher** na wdrożonej stronie: panel się ładuje, połączenie się otwiera, twoja wiadomość trafia na Slacka.

## gdzie to wdrażasz

Nic tu nie zależy od tego, gdzie działa appka. Vercel, serwer Node, kontener albo `output: "export"` dla statycznej strony serwują ten sam HTML, a tag działa w każdym tak samo. Przy statycznym eksporcie zalogowany user musi przyjść z serwera, który kontrolujesz, bo nie ma renderu po stronie serwera, który by go podpisał.

## podepnij odpowiedzi

Kiedy czat jest już na stronie, podłącz Slacka w dashboardzie i wybierz kanał. Przeprowadzi cię przez to [poradnik o Slacku](/pl/blog/answer-website-chat-from-slack/). Jeśli używasz Claude Code, [poradnik o Claude Code](/pl/blog/claude-code-live-chat/) pokazuje, jak dać agentowi zrobić wszystko powyżej.

## appki wielojęzyczne

Jeśli twoja appka ma segment `[locale]`, powiedz widżetowi, w jakim języku jest strona, żeby panel pasował do strony, a nie do przeglądarki odwiedzającego. Widżet mówi w 12 językach; `lang` przyjmuje kody typu `en`, `pl`, `de`. W `app/[locale]/layout.tsx`:

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

Język, którego widżet nie ma, wraca do języka przeglądarki odwiedzającego, a potem do angielskiego. Twój zespół i tak widzi język przy czacie.

## częste błędy

- **Tag w client componencie z `"use client"`.** Renderuje się, ale hydruje się razem z komponentem, a każdy remount to kolejna szansa na duplikat. Trzymaj go w layoucie.
- **Tworzenie skryptu w `useEffect`.** Więcej kodu na ten sam efekt, a ładuje się później niż `afterInteractive`.
- **`type="module"` na tagu.** Launcher to klasyczny skrypt; nie dawaj typu.
- **Preload panelu.** `<link rel="preload">` dla skryptu panelu przesuwa jego pobieranie przed kliknięcie dla każdego odwiedzającego. Cały sens jest w tym, że czeka.
- **Testowanie czatu na preview deploymencie.** Adresy preview to inne domeny. Launcher się pokaże, czat się nie połączy. Tak ma być.

## ile to dokłada do bundla

Nic. Launcher przychodzi z naszego CDN jako osobny plik, więc nie jest częścią twojego bundla JavaScriptu, a liczby „First Load JS”, które wypisuje `next build`, zostają dokładnie takie same. Nie dotyka też Reacta: przycisk żyje we własnym shadow roocie, więc twój CSS go nie zepsuje, a jego CSS nie wycieknie do twojej appki. Twój build musi wiedzieć tylko o jednym tagu.

## checklista

- Jeden tag albo `<Script>` w głównym layoucie (`app/layout.tsx` albo `pages/_app.tsx`).
- Konfiguracja, jeśli jest, odpala się przed launcherem.
- Podpisy userów liczone tylko na serwerze.
- CSP wpuszcza CDN i połączenie czatu, jeśli masz CSP.
- Sprawdzone na produkcyjnym buildzie: jeden request przed kliknięciem (dwa, oba z `cdn.spookat.com`, jeśli twój kod ustawia styl, którego nie opublikowałeś), zero cookies.

Kosztuje 19 zł miesięcznie za stronę na starter, a trial trwa 14 dni bez karty.

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