Cara Menambahkan Live Chat di Next.js (App Router dan Pages Router)
Panduan widget chat selalu bilang tempel tag sebelum penutup body. Next.js tidak punya file seperti itu. Ini tempat script yang benar, dan apa yang terjadi saat navigasi di sisi klien.
Untuk menambahkan live chat di Next.js, taruh script widget di root layout, yaitu app/layout.tsx di App Router atau pages/_app.tsx di Pages Router, memakai komponen <Script> dari next/script dengan strategy="afterInteractive" atau "lazyOnload". Cukup dideklarasikan sekali, semua route langsung punya chat, dan script tidak dijalankan ulang saat pengunjung berpindah halaman.
Ringkasan
- Pakai
next/scriptdi root layout (App Router) ataupages/_app.tsx(Pages Router), jangan tag<script>biasa di JSX atau dipages/_document.tsx. - Pilih
afterInteractiveagar tombol chat cepat muncul, ataulazyOnloaduntuk menjaga skor performa; dokumentasi Next.js sendiri menyebut plugin chat sebagai contohlazyOnload. - Navigasi di sisi klien tidak memuat script lagi: satu tag, satu widget, dan jejak halaman pengunjung tetap mengikuti route.
- Panggil
window.SealReplyhanya dari client component, di event handler atauuseEffect, dan lindungi dengan?.. - Untuk menyembunyikan chat di route tertentu, pakai aturan halaman: widget memeriksanya lagi di setiap navigasi sisi klien.
Halaman instalasi widget chat mana pun biasanya bilang "tempel kode ini sebelum </body>". Next.js tidak punya file HTML untuk ditempeli, dan tag <script> mentah yang Anda taruh di JSX tidak akan berjalan seperti yang Anda bayangkan. Panduan ini menunjukkan pola yang benar-benar berhasil, semuanya sudah diuji pada build produksi Next.js 16.4.6 di kedua router.
Langkah 1: Ambil site key Anda
Kode sematan didapat dari konsol chat, dan satu-satunya bagian yang dibutuhkan Next.js adalah site key.
- Daftar akun SealReply gratis. Konsol akan membuka Siapkan chat Anda.
- Di bagian Situs web Anda, isi Nama situs dan Alamat situs web tempat aplikasi Anda di-deploy, misalnya
app.example.com, lalu klik Lanjutkan. - Di langkah Pasang widget, biarkan Situs web Anda memakai pada HTML, lalu klik Salin.

Anda akan mendapatkan satu baris kode:
<script src="https://cdn.sealreply.com/widget.js" data-site-key="sk_…" async></script>
Simpan nilai src dan data-site-key; di langkah berikutnya keduanya diubah menjadi komponen <Script>. Site key bukan rahasia, karena toh muncul di HTML setiap halaman, jadi boleh ditulis langsung di kode atau disimpan di environment variable berawalan NEXT_PUBLIC_.
Langkah 2: Di mana script chat ditaruh di App Router?
Taruh di app/layout.tsx, yaitu root layout, supaya semua route mendapatkannya:
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://cdn.sealreply.com/widget.js"
data-site-key="sk_…"
strategy="afterInteractive"
/>
</body>
</html>
);
}
Ada dua hal yang perlu Anda ketahui:
- Atribut
data-*tetap terbawa.<Script>meneruskan atribut tambahan ke elemen script yang dibuatnya. Ini penting karena widget membaca key-nya daridocument.currentScript.dataset. Di halaman hasil render, tag tersebut membawadata-site-keydan bersifatasync. Panduan memuat script di Next.js menjelaskannya di bagian "Additional Attributes". - Server component tidak masalah.
<Script>sendiri boleh berada di server component. Hanya callbackonLoad,onReady, danonErroryang butuh client component ("use client"di baris paling atas file).
Langkah 2 (Pages Router): pages/_app.tsx
Di Pages Router, script ditaruh di pages/_app.tsx, file yang membungkus setiap halaman:
// 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.sealreply.com/widget.js"
data-site-key="sk_…"
strategy="afterInteractive"
/>
</>
);
}
Jangan di _document
Jangan menaruhnya di pages/_document.tsx. File itu hanya dirender di server, dan next/script di sana ditujukan untuk strategy beforeInteractive, yang tidak cocok untuk widget chat.
Langkah 3: Strategy apa yang cocok untuk widget chat?
Pakai afterInteractive jika Anda ingin tombol chat cepat muncul, atau lazyOnload jika skor performa lebih penting. Referensi API next/script menjelaskan keempat strategy:
| strategy | Kapan dimuat | Cocok untuk |
|---|---|---|
beforeInteractive |
Sebelum kode Next.js mana pun | Deteksi bot, persetujuan cookie. Hanya di root layout; bukan untuk chat |
afterInteractive (default) |
Setelah sebagian hydration selesai | Analitik, tag manager, dan chat jika Anda ingin tombolnya cepat tampil |
lazyOnload |
Saat browser sedang idle | Yang disebut dokumentasi Next.js untuk plugin chat support |
worker (eksperimental) |
Di web worker | Bukan untuk chat: belum berjalan di App Router, dan widget butuh DOM halaman |
lazyOnload paling ramah untuk skor Lighthouse, dengan konsekuensi tombol chat muncul sedikit lebih lambat. afterInteractive menampilkan tombol lebih cepat, cocok untuk landing page dan halaman harga, tempat pertanyaan bisa datang kapan saja. Widget SealReply berukuran sekitar 38 KB dan dimuat secara asinkron, jadi strategy mana pun tidak menahan tampilnya konten Anda.
Ukur, jangan menebak
Jalankan PageSpeed Insights di halaman yang sama, sekali dengan tiap strategy, lalu bandingkan LCP dan TBT. Deploy setiap versi ke URL yang sama supaya tidak ada hal lain yang berubah di antara dua pengujian.
Langkah 4: Pastikan chat sudah berjalan
Deploy, buka situs live Anda, dan cari tombol chat di pojok layar. Kunjungan pertama ke beranda juga memverifikasi domain Anda, dan widget baru tampil setelah domain terverifikasi.
Lalu buka console browser dan jalankan:
document.querySelectorAll('script[src*="widget.js"]').length; // 1
window.SealReply?.version; // the widget's version
Hasil yang Anda inginkan: tepat satu tag dan sebuah string versi. Klik beberapa halaman lewat <Link>, lalu jalankan baris pertama lagi: hasilnya harus tetap 1. Terakhir, kirim pesan ke diri sendiri dari widget dan balas dari konsol, supaya Anda yakin kedua sisi berfungsi.
Apakah widget dimuat ulang saat navigasi di sisi klien?
Tidak. Navigasi di sisi klien tidak memuat ulang dokumen, jadi script tidak dijalankan lagi, dan memang itu yang Anda mau. Dokumentasi Next.js menyebut script di root layout "will only load once, even if a user navigates between multiple pages". Pada build produksi Next.js 16.4.6, saat berpindah dari beranda ke /pricing lewat <Link> lalu menekan tombol back browser:
- tetap hanya ada satu tag
widget.jsdi halaman, dan widget diinisialisasi sekali; - tombol chat tetap di tempatnya, tanpa berkedip dan tanpa reload;
- catatan kunjungan di widget terbaca
/→/pricing→/, termasuk tombol back.

Poin terakhir itu perlu sedikit penjelasan. Widget yang menangani single-page app dengan benar membungkus history.pushState dan replaceState, serta mendengarkan popstate dan hashchange. Dengan begitu, "halaman saat ini" yang dilihat agen Anda mengikuti route, bukan berhenti di halaman pertama tempat pengunjung mendarat. Pesan sambutan dan undangan chat per halaman juga mengikuti route dengan cara yang sama. Hal ini layak diuji saat Anda memilih tool, karena banyak widget hanya melaporkan halaman sekali per pemuatan dokumen dan tidak pernah lagi.
Satu catatan lagi: meskipun Anda mendeklarasikan <Script> di dalam satu halaman, bukan di root layout, sebuah src tetap hanya dijalankan sekali per pemuatan dokumen. Buka route itu dua kali, dan penghitung di dalam script tetap bernilai 1. Di mode development, React Strict Mode me-mount komponen dua kali, dan tag script tetap hanya satu.
Bagaimana membuka chat dari tombol Anda sendiri?
Tambahkan data-launcher="none" ke kode sematan, lalu panggil window.SealReply.open() dari client component:
"use client";
export function ContactButton() {
return (
<button type="button" onClick={() => window.SealReply?.open()}>
Chat with us
</button>
);
}
Dengan data-launcher="none", tidak ada tombol di pojok dan tidak ada gelembung undangan yang muncul; chat hanya terbuka saat kode Anda memanggil open(). Script-nya async, jadi mungkin belum siap ketika ada yang mengklik terlalu cepat. Optional chaining sudah cukup sebagai pengaman. Jika Anda ingin menjalankan sesuatu begitu widget siap, misalnya menghitung balasan, tunggu event ready-nya:
"use client";
import { useEffect, useState } from "react";
export function ChatButton() {
const [unread, setUnread] = useState(0);
useEffect(() => {
const onMessage = () => setUnread((n) => n + 1);
const subscribe = () => window.SealReply?.on("message", onMessage);
if (window.SealReply) subscribe();
else window.addEventListener("sealreply:ready", subscribe, { once: true });
return () => {
window.removeEventListener("sealreply:ready", subscribe);
window.SealReply?.off("message", onMessage);
};
}, []);
return (
<button type="button" onClick={() => window.SealReply?.open()}>
Chat with us{unread > 0 ? ` (${unread})` : ""}
</button>
);
}
Di proyek TypeScript, satu file deklarasi sudah cukup untuk memberi tipe pada window.SealReply:
// types/sealreply.d.ts
export type SealReplyMessage = {
id: string;
text: string | null;
content_type: string;
created_at: string;
};
declare global {
interface Window {
SealReply?: {
open(): void;
close(): void;
on(event: "ready" | "open" | "close" | "message", handler: (data?: SealReplyMessage) => void): void;
off(event: "ready" | "open" | "close" | "message", handler: (data?: SealReplyMessage) => void): void;
identify(identity: { external_id: string; identity_hash: string; name?: string; email?: string }): void;
reset(): void;
version: string;
};
}
}
Bagaimana menyembunyikan chat di route tertentu?
Gunakan aturan halaman: Situs → situs Anda → Aturan halaman → Halaman tanpa chat. Tidak perlu kode, dan aturan ini berjalan di single-page app. Widget memeriksa aturan lagi di setiap navigasi sisi klien: saat pengunjung masuk ke route yang dikecualikan, chat tertutup dan tombolnya hilang; saat mereka keluar, tombolnya kembali. Path diawali /app mencakup seluruh dashboard, sedangkan Path sama dengan /checkout mencakup satu halaman. Maksimal 20 aturan per situs.
Anda juga bisa menjauhkan script dari sebagian aplikasi lewat kode, dengan menaruh <Script> di layout route group seperti app/(marketing)/layout.tsx, bukan di root layout. Cara ini mencegah script dimuat untuk pengunjung yang langsung datang ke, misalnya, dashboard Anda.
Unmount tidak berarti unload
Begitu script sudah berjalan, menghapus <Script> dari tree tidak menghapus widget: Next.js tidak membongkar script yang sudah dieksekusi. Pengunjung yang mendarat di halaman marketing lalu masuk ke /app tetap melihat widget. Jadi pasangkan layout route group dengan aturan halaman jika route tersebut harus benar-benar bersih dari chat.
Bagaimana menampilkan nama pengguna yang sudah login?
Panggil SealReply.identify() setelah pengguna login, dengan signature yang dihitung di server Anda:
window.SealReply?.identify({
external_id: user.id,
identity_hash: hashFromServer, // HMAC-SHA256, computed on your server
name: user.name,
email: user.email,
});
Pertama, aktifkan Verifikasi identitas pengunjung untuk situs tersebut dan salin rahasianya; rahasia ini hanya ditampilkan sekali. Hitung signature hanya di server, yaitu di route handler (app/api/.../route.ts) atau server component, dengan rahasia disimpan di environment variable tanpa awalan NEXT_PUBLIC_ agar tidak pernah sampai ke browser. Widget akan dimuat ulang sebagai pengguna tersebut, dan agen melihatnya dengan tanda terverifikasi. Panggil SealReply.reset() saat pengguna logout. Panduan lengkapnya ada di cara mengidentifikasi pengguna yang login di live chat, dan contoh kode dalam empat bahasa ada di dokumentasi widget.
Bagaimana jika chat tidak muncul?
Sebagian besar masalah berasal dari mengakses window di server, menguji di localhost, atau Content Security Policy:
| Masalah | Penyebab dan solusi |
|---|---|
ReferenceError: window is not defined |
Anda mengakses window di server component atau saat render. Pakai di event handler atau useEffect, di dalam komponen "use client". |
| Tombol chat tidak muncul di localhost | Widget hanya tampil di domain yang diizinkan untuk situs itu beserta subdomainnya, dan localhost tidak bisa ditambahkan. Uji di situs yang sudah di-deploy, atau di subdomain staging seperti staging.example.com. Domain utama harus terverifikasi dulu: buka beranda live Anda sekali setelah kode di-deploy. |
| Ada dua tombol chat | Script yang sama dideklarasikan dua kali, misalnya sekali di root layout dan sekali di sebuah halaman dengan strategy berbeda. Sisakan satu. |
| Tidak ada yang termuat setelah Anda menambahkan CSP | Izinkan script-src https://cdn.sealreply.com, connect-src https://api.sealreply.com wss://rt.sealreply.com, dan img-src https://api.sealreply.com. Jika CSP Anda berbasis nonce, teruskan juga nonce ke <Script>; lihat panduan Content Security Policy Next.js. |
| Anda ingin chat dimuat hanya saat diklik | Render <Script> di balik sebuah state dan ubah state itu dari tombol Anda sendiri; next/script menyisipkan tag pada saat itu. |
Pertanyaan umum
Di mana script widget chat ditaruh di Next.js?
Taruh di root layout, app/layout.tsx, untuk App Router, atau di pages/_app.tsx untuk Pages Router, memakai komponen <Script> dari next/script. Dengan dideklarasikan sekali di sana, script dimuat di semua route dan tidak dijalankan ulang saat navigasi di sisi klien.
Widget chat sebaiknya memakai afterInteractive atau lazyOnload?
Keduanya bisa; tergantung mana yang lebih Anda pentingkan. afterInteractive menampilkan tombol chat segera setelah halaman interaktif, sedangkan lazyOnload menunggu browser idle dan lebih ringan untuk skor performa. Dokumentasi Next.js menyebut plugin chat support sebagai contoh penggunaan lazyOnload.
Kenapa memanggil window.SealReply menghasilkan "window is not defined"?
Karena kodenya berjalan di server, dan di server tidak ada window. Pindahkan pemanggilan ke komponen "use client" dan jalankan di event handler atau useEffect, yang hanya berjalan di browser.
Bisakah widget chat diuji di localhost?
Tidak. Widget hanya dimuat di domain yang diizinkan untuk situs itu, dan localhost maupun alamat IP tidak bisa ditambahkan ke daftar tersebut. Uji di preview yang sudah di-deploy pada subdomain dari domain terverifikasi Anda, seperti staging.example.com, atau langsung di situs live.
Apakah CSS global atau Tailwind saya akan merusak tampilan widget chat?
Tidak. Widget dirender di dalam Shadow DOM, jadi CSS halaman Anda tidak menjangkaunya dan CSS widget tidak bocor ke aplikasi Anda. Warna, posisi, dan tombol peluncur diatur di bagian Tampilan di konsol.
Amankah menaruh site key di kode Next.js?
Aman. Site key memang muncul di HTML setiap halaman, dan widget hanya tampil di domain yang Anda izinkan untuk situs itu, jadi orang yang menyalinnya ke situs mereka sendiri tidak akan mendapatkan chat. Nilai yang wajib dirahasiakan adalah rahasia verifikasi identitas, yang tempatnya di environment variable khusus server.
Langkah berikutnya
Setelah widget live, hubungkan apa yang terjadi di dalamnya ke sistem Anda yang lain: verifikasi signature webhook agar Anda tahu request benar-benar datang dari kami, lalu ikuti kirim notifikasi live chat ke Slack dan Telegram untuk meneruskan pertanyaan baru ke tempat tim Anda biasa berkumpul. Webhook tersedia mulai paket Starter.
Listener SealReply.on("open", …) yang sama juga jadi pintu masuk analitik: cara tracking konversi live chat di Google Analytics 4 menjelaskan di mana memasangnya dalam aplikasi satu halaman agar event tidak berlipat setiap ganti rute.
Masih memilih tool? Mulai dari cara memasang live chat di situs web. Sudah terpasang? Mulai menggunakan SealReply membahas cara menjawab dan menindaklanjuti percakapan.