vraag beantwoord, verkoop geredde chat hoeft geen cookiebanneronbeperkt teamledenantwoord vanuit slack of onze app

/blog / handleidingen

live chat op een next.js-site

· door kuba · 6 min lezen

Een stekker met het label next.js vlak voor een roze stopcontact, met de Spookat-kat op de kabel.

Live chat in Next.js is één script-tag in de root-layout. Deze gids laat zien waar die komt in de app router en de pages router, hoe je de look van de widget en de ingelogde gebruiker doorgeeft zonder de laadvolgorde te breken, wat er in een Content Security Policy moet en hoe je het resultaat controleert in een productiebuild.

de korte versie

In de app router voeg je de tag toe aan app/layout.tsx, binnen <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>
  );
}

Vervang YOUR_SITE_KEY door de key van de installatiepagina in het dashboard. Dat is de hele installatie als de look in het dashboard staat, en dat is de standaard. De rest van deze gids is voor als je meer controle wilt. Begin je vanaf nul, dan behandelt de basisgids de accountkant.

waarom de root-layout

De root-layout omsluit elke route en blijft gemount terwijl de gebruiker navigeert. De launcher hecht zich één keer aan de pagina. Navigatie aan de clientkant herlaadt de pagina niet, dus ook de launcher niet, en een bezoeker kan een gesprek openhouden terwijl die rondklikt.

Zet je de tag in een paginacomponent, dan bestaat de chat alleen op die pagina. Zet je hem in een component dat op meerdere pagina’s rendert, dan krijg je bij harde reloads dubbele exemplaren (de launcher negeert zijn eigen tweede kopie, maar het blijft een verspild request).

met next/script

next/script is de Next.js-manier om scripts van derden te laden, en het werkt hier prima:

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 laadt de launcher direct na de hydration. lazyOnload wacht tot de browser idle is, waardoor de knop op trage telefoons iets later verschijnt. Beide zijn prima voor een launcher van 2 kb; afterInteractive toont de knop eerder. Gebruik geen beforeInteractive voor de launcher: die zou voor niets met je eigen code concurreren.

de look in code instellen

De look die je in het dashboard publiceert, zit in de launcher. Hou je hem liever in de repo, zet dan window.Spookat voordat de launcher draait. De config van de pagina wint van die van het dashboard.

De laadvolgorde telt: de config moet bestaan als de launcher wordt uitgevoerd. Met next/script geef je de config beforeInteractive (toegestaan in de root-layout) en de launcher 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>
  );
}

beforeInteractive-scripts draaien voor de hydration, afterInteractive-scripts erna, dus de volgorde is gegarandeerd. De keys zijn dezelfde als overal: style (een van 8), font, accent, side, greeting, label, lang. De docs noemen ze allemaal.

de pages router

In de pages router is de gedeelde wrapper 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" />
    </>
  );
}

Voor een config horen beforeInteractive-scripts in de pages router in pages/_document.tsx. Zet het window.Spookat-script daar, binnen <Head>, met dezelfde id en strategy als hierboven, en laat de launcher in _app.tsx.

de ingelogde gebruiker

Vanaf pro kun je de chat vertellen wie er ingelogd is. Je team ziet dan logged in as 42 op de Slack-kaart, en webhooks, de export en read_convo van je agent dragen de id mee. De id wordt ondertekend met een secret dat alleen jouw server kent, zodat niemand zich als iemand anders kan voordoen.

Maak het identity-secret op de installatiepagina van het dashboard, zet het in een omgevingsvariabele en onderteken op de server. In de app router is de root-layout een servercomponent, en dat maakt het eenvoudig:

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

Bereken de handtekening nooit in een clientcomponent: dan zou het secret naar de browser gaan. Een ontbrekende of verkeerde handtekening is geen fout; de chat zegt dan gewoon niet wie het is. Lees je de sessie in de root-layout, dan rendert elke pagina per request. Is je layout nu statisch, weeg dat dan af voordat je het toevoegt.

content security policy

Stuurt je app een Content-Security-Policy-header, dan heeft de widget nodig:

  • script-src en style-src: https://cdn.spookat.com
  • font-src: https://cdn.spookat.com, als je een gehost font koos
  • connect-src: wss://api.spookat.com, voor de chatverbinding na een klik
  • img-src: data:, voor logo’s en foto’s van je teamgenoten
  • style-src: de launcher voegt een klein <style>-blok toe aan zijn eigen shadow root, dus inline styles moeten voor hem zijn toegestaan

Zonder deze regels vertelt de browserconsole precies welke directive wat blokkeerde. Voeg de genoemde bron toe en herlaad.

controleer het in een productiebuild

Dev-modus voegt eigen scripts en hot reload toe, dus controleer een echte build:

next build && next start

Open http://localhost:3000 in een privévenster met DevTools open. Het domein telt: de chat maakt alleen verbinding vanaf het domein dat je registreerde en de subdomeinen ervan, dus op localhost zie je de launcher, maar maakt de chat geen verbinding. Test de chat zelf op je gedeployde site.

Op allebei:

  1. Network, “Disable cache”, herladen, filteren op spookat: één request, de launcher van cdn.spookat.com. Twee, allebei van cdn.spookat.com, als je code in window.Spookat een andere stijl zet dan die je gepubliceerd hebt. Niets naar api.spookat.com.
  2. Application: geen cookies van Spookat en geen opslag tot een eerste bericht.
  3. Performance of Lighthouse: de launcher hoort niet als long task te verschijnen. Vergelijk een paar runs met en zonder; het verschil hoort binnen de normale ruis tussen runs te vallen.
  4. Klik op de launcher op de gedeployde site: het paneel laadt, de verbinding opent, je bericht komt aan in Slack.

deploydoelen

Niets hiervan hangt af van waar de app draait. Vercel, een Node-server, een container of output: "export" voor een statische site serveren allemaal dezelfde HTML, en de tag werkt overal hetzelfde. Bij een statische export moet de ingelogde gebruiker van een server komen die jij beheert, omdat er geen serverrender is om hem te ondertekenen.

koppel de antwoorden

Staat de chat op de site, koppel dan Slack in het dashboard en kies een kanaal. De Slack-gids loopt het met je door. Gebruik je Claude Code, dan laat de Claude Code-gids zien hoe de agent al het bovenstaande kan doen.

meertalige apps

Heeft je app een [locale]-segment, vertel de widget dan in welke taal de pagina is, zodat het paneel bij de pagina past en niet bij de browser van de bezoeker. De widget spreekt 12 talen; lang neemt codes zoals en, pl, de. In app/[locale]/layout.tsx:

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

Een taal die de widget niet heeft, valt terug op de browser van de bezoeker, daarna op Engels. Je team ziet de taal hoe dan ook bij de chat.

veelgemaakte fouten

  • De tag in een clientcomponent met "use client". Hij rendert, maar wordt samen met het component gehydrateerd, en elke remount is weer een kans op een duplicaat. Hou hem in de layout.
  • Het script aanmaken in useEffect. Meer code voor hetzelfde resultaat, en het laadt later dan afterInteractive.
  • type="module" op de tag. De launcher is een klassiek script; laat het type weg.
  • Het paneel vooraf laden. Een <link rel="preload"> voor het paneelscript zet de download voor elke bezoeker voor de klik. Het hele punt is dat het wacht.
  • De chat testen op een preview-deployment. Preview-URL’s zijn andere domeinen. De launcher verschijnt, de chat maakt geen verbinding. Dat is zo bedoeld.

wat het aan je bundle toevoegt

Niets. De launcher komt als eigen bestand van ons CDN, dus hij hoort niet bij je JavaScript-bundle, en de “First Load JS”-cijfers die next build toont blijven precies gelijk. Hij komt ook niet aan React: de knop leeft in zijn eigen shadow root, dus je CSS kan hem niet breken en zijn CSS kan niet in je app lekken. Het enige wat je build hoeft te weten, is die ene tag.

checklist

  • Eén tag of <Script> in de root-layout (app/layout.tsx of pages/_app.tsx).
  • De config, als die er is, draait voor de launcher.
  • Gebruikershandtekeningen worden alleen op de server berekend.
  • De CSP staat het CDN en de chatverbinding toe, als je een CSP hebt.
  • Gecontroleerd in een productiebuild: één request voor een klik (twee, allebei van cdn.spookat.com, als je code een stijl zet die je niet gepubliceerd hebt), geen cookies.

Het kost € 4,99 per maand per site op starter, en de proefperiode is 14 dagen zonder kaart.

goed? zet je op de lijst.

uitnodigingen gaan in kleine groepjes de deur uit. is de jouwe binnen: 14 dagen gratis, geen creditcard.

we mailen je alleen over je uitnodiging en de lancering. afmelden kan altijd. privacy