odpisane pytanie, uratowana sprzedażczat bez banera cookiescały zespół bez limituodpowiadasz ze slacka albo z naszej apki

/blog / poradniki

dodaj live chat do strony w next.js

· pisze kuba · 5 min czytania

Wtyczka z napisem next.js tuż przed różowym gniazdkiem, kot Spookat jedzie na kablu.

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

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. 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.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.

podoba się? zapisz się na listę.

zaproszenia wysyłamy małymi partiami. gdy przyjdzie twoje: 14 dni za darmo, bez karty.

piszemy tylko w sprawie zaproszenia i startu. możesz to wyłączyć w każdej chwili. prywatność