dodaj live chat do strony w next.js

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>:
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 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:
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:
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 wymienia wszystkie.
pages router
W pages routerze wspólny wrapper to 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" />
</>
);
}
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:
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-srcistyle-src:https://cdn.spookat.comfont-src:https://cdn.spookat.com, jeśli wybrałeś font hostowany u nasconnect-src:wss://api.spookat.com, na połączenie czatu po kliknięciuimg-src:data:, na logo i zdjęcia ludzi z zespołustyle-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:
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:
- Network, „Disable cache”, przeładuj, filtr
spookat: jeden request, launcher zcdn.spookat.com. Dwa, oba zcdn.spookat.com, jeśli twój kod ustawia wwindow.Spookatinny styl niż ten, który opublikowałeś. Nic doapi.spookat.com. - Application: żadnych cookies od Spookat i żadnego storage do pierwszej wiadomości.
- 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.
- 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. Jeśli używasz Claude Code, poradnik o Claude Code 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:
<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.tsxalbopages/_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.