# tambahkan live chat ke situs next.js

Tambahkan chat Spookat ke aplikasi Next.js, di app router atau pages router, lengkap dengan tampilan dan pengguna yang login bila perlu, lalu pastikan tidak ada request ke API kami sebelum klik.

2026-11-04 · Kuba

Live chat di Next.js cukup satu tag script di root layout. Panduan ini menunjukkan di mana tag itu ditaruh di app router dan pages router, cara mengirim tampilan widget dan pengguna yang sedang login tanpa merusak urutan muat, apa yang perlu ditambahkan ke Content Security Policy, dan cara mengecek hasilnya di build produksi.

## versi singkatnya

Di app router, tambahkan tag ke `app/layout.tsx`, di dalam `<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>
  );
}
```

Ganti `YOUR_SITE_KEY` dengan key dari halaman instalasi di dashboard. Itu seluruh instalasinya kalau tampilan diatur di dashboard, dan memang itu default-nya. Sisa panduan ini untuk kalau kamu ingin kontrol lebih. Kalau mulai dari nol, [panduan utama](/id/blog/add-live-chat-to-your-website/) membahas sisi akunnya.

## kenapa root layout

Root layout membungkus setiap route dan tetap terpasang selama pengguna berpindah halaman. Launcher menempel ke halaman sekali saja. Navigasi di sisi klien tidak me-reload halaman, jadi launcher juga tidak di-reload, dan pengunjung bisa tetap membuka percakapan sambil mengklik ke mana-mana.

Kalau tag ditaruh di komponen halaman, chat hanya ada di halaman itu. Kalau ditaruh di komponen yang dirender di beberapa halaman, kamu akan dapat duplikat saat hard reload (launcher mengabaikan salinan keduanya sendiri, tapi tetap saja itu request yang terbuang).

## dengan next/script

`next/script` adalah cara Next.js memuat script pihak ketiga, dan di sini berfungsi baik:

```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` memuat launcher tepat setelah hydration. `lazyOnload` menunggu sampai browser idle, sehingga tombolnya muncul sedikit lebih lambat di HP yang lambat. Keduanya oke untuk launcher 2 kb; `afterInteractive` menampilkan tombol lebih cepat. Jangan pakai `beforeInteractive` untuk launcher: ia akan bersaing dengan kodemu sendiri tanpa guna.

## mengatur tampilan lewat kode

Tampilan yang kamu publish di dashboard terbawa di dalam launcher. Kalau kamu lebih suka menyimpannya di repo, set `window.Spookat` sebelum launcher berjalan. Config di halaman mengalahkan config dashboard.

Urutan muat itu penting: config harus sudah ada saat launcher dijalankan. Dengan `next/script`, beri config `beforeInteractive` (diizinkan di root layout) dan launcher `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>
  );
}
```

Script `beforeInteractive` berjalan sebelum hydration, `afterInteractive` sesudahnya, jadi urutannya terjamin. Key-nya sama seperti di tempat lain: `style` (salah satu dari 8), `font`, `accent`, `side`, `greeting`, `label`, `lang`. [Dokumentasi](/id/docs/) mendaftar semuanya.

## pages router

Di pages router, wrapper bersamanya adalah `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" />
    </>
  );
}
```

Untuk config, di pages router script `beforeInteractive` ditaruh di `pages/_document.tsx`. Taruh script `window.Spookat` di sana, di dalam `<Head>`, dengan `id` dan `strategy` yang sama seperti di atas, dan biarkan launcher di `_app.tsx`.

## pengguna yang sedang login

Mulai pro, kamu bisa memberi tahu chat siapa yang sedang login. Timmu lalu melihat `logged in as 42` di kartu Slack, dan webhook, ekspor, serta `read_convo` milik agenmu membawa id-nya. Id itu ditandatangani dengan secret yang hanya diketahui servermu, jadi tidak ada yang bisa menyamar sebagai orang lain.

Buat identity secret di halaman instalasi dashboard, simpan di environment variable, lalu tandatangani di server. Di app router, root layout adalah server component, jadi ini mudah:

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

Jangan pernah menghitung tanda tangan di client component: secret-nya akan ikut terkirim ke browser. Tanda tangan yang hilang atau salah bukan error; chat hanya tidak menyebutkan siapa penggunanya. Membaca session di root layout membuat setiap halaman dirender per request, jadi kalau layout-mu sekarang statis, pertimbangkan dulu sebelum menambahkannya.

## content security policy

Kalau aplikasimu mengirim header `Content-Security-Policy`, widget butuh:

- `script-src` dan `style-src`: `https://cdn.spookat.com`
- `font-src`: `https://cdn.spookat.com`, kalau kamu memilih font yang di-host
- `connect-src`: `wss://api.spookat.com`, untuk koneksi chat setelah klik
- `img-src`: `data:`, untuk logo dan foto rekan tim
- `style-src`: launcher menyisipkan satu blok `<style>` kecil ke shadow root-nya sendiri, jadi inline style harus diizinkan untuknya

Tanpa ini, console browser akan menyebut persis direktif mana yang memblokir apa. Tambahkan sumber yang disebut, lalu reload.

## cek di build produksi

Mode dev menambahkan script-nya sendiri dan hot reload, jadi cek build yang sungguhan:

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

Buka `http://localhost:3000` di jendela private dengan DevTools terbuka. Domain itu penting: chat hanya terhubung dari domain yang kamu daftarkan dan subdomainnya, jadi di localhost kamu akan melihat launcher, tapi chat tidak terhubung. Tes chat-nya sendiri di situs yang sudah di-deploy.

Di keduanya:

1. **Network**, "Disable cache", reload, filter `spookat`: satu request, launcher dari `cdn.spookat.com`. Dua, keduanya dari `cdn.spookat.com`, kalau kodemu mengatur style di `window.Spookat` yang bukan style yang kamu publikasikan. Tidak ada ke `api.spookat.com`.
2. **Application**: tidak ada cookie dari Spookat, dan tidak ada penyimpanan sampai pesan pertama.
3. **Performance atau Lighthouse**: launcher tidak boleh muncul sebagai long task. Bandingkan beberapa run dengan dan tanpa launcher; selisihnya seharusnya masih di dalam noise normal antar-run.
4. **Klik launcher** di situs yang sudah di-deploy: panel dimuat, koneksi terbuka, pesanmu sampai ke Slack.

## target deploy

Tidak ada yang bergantung pada di mana aplikasi berjalan. Vercel, server Node, container, atau `output: "export"` untuk situs statis semuanya menyajikan HTML yang sama, dan tag-nya berfungsi sama di mana pun. Untuk export statis, pengguna yang login harus datang dari server yang kamu kendalikan, karena tidak ada server render untuk menandatanganinya.

## hubungkan balasannya

Setelah chat ada di situs, hubungkan Slack di dashboard dan pilih channel. [Panduan Slack](/id/blog/answer-website-chat-from-slack/) memandumu. Kalau kamu pakai Claude Code, [panduan Claude Code](/id/blog/claude-code-live-chat/) menunjukkan cara membiarkan agent mengerjakan semua hal di atas.

## aplikasi multibahasa

Kalau aplikasimu punya segmen `[locale]`, beri tahu widget bahasa halamannya, supaya panel mengikuti halaman, bukan browser pengunjung. Widget ini bisa 12 bahasa; `lang` menerima kode seperti `en`, `pl`, `de`. Di `app/[locale]/layout.tsx`:

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

Bahasa yang tidak dimiliki widget akan jatuh ke bahasa browser pengunjung, lalu ke bahasa Inggris. Timmu tetap melihat bahasanya di chat.

## kesalahan umum

- **Tag di dalam client component dengan `"use client"`.** Tetap dirender, tapi ikut di-hydrate bersama komponennya, dan setiap remount jadi peluang baru untuk duplikat. Simpan di layout.
- **Membuat script di `useEffect`.** Lebih banyak kode untuk hasil yang sama, dan dimuat lebih lambat daripada `afterInteractive`.
- **`type="module"` di tag.** Launcher adalah script klasik; jangan pakai type.
- **Preload panel.** `<link rel="preload">` untuk script panel memindahkan unduhannya ke sebelum klik untuk setiap pengunjung. Intinya justru panel itu menunggu.
- **Tes chat di deployment preview.** URL preview itu domain lain. Launcher muncul, chat tidak terhubung. Itu memang begitu.

## apa yang ditambahkan ke bundle-mu

Tidak ada. Launcher datang dari CDN kami sebagai file tersendiri, jadi bukan bagian dari bundle JavaScript-mu, dan angka "First Load JS" yang dicetak `next build` tetap sama persis. React juga tidak disentuh: tombolnya hidup di shadow root sendiri, jadi CSS-mu tidak bisa merusaknya dan CSS-nya tidak bisa bocor ke aplikasimu. Satu-satunya yang perlu diketahui build-mu hanyalah tag itu.

## checklist

- Satu tag atau `<Script>` di root layout (`app/layout.tsx` atau `pages/_app.tsx`).
- Config, kalau ada, berjalan sebelum launcher.
- Tanda tangan pengguna dihitung hanya di server.
- CSP mengizinkan CDN dan koneksi chat, kalau kamu punya CSP.
- Sudah dicek di build produksi: satu request sebelum klik (dua, keduanya dari `cdn.spookat.com`, kalau kodemu mengatur style yang tidak kamu publikasikan), tanpa cookie.

Biayanya $4.99 per bulan per situs di starter, dan trial-nya 14 hari tanpa kartu.

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