bir next.js sitesine canlı sohbet ekleyin

Next.js’te canlı sohbet, kök layout’ta tek bir script etiketidir. Bu rehber, etiketin app router ve pages router’da nereye gittiğini, yükleme sırasını bozmadan widget’ın görünümünü ve giriş yapmış kullanıcıyı nasıl ileteceğinizi, bir Content Security Policy’ye neler ekleneceğini ve sonucu bir production build’de nasıl kontrol edeceğinizi gösteriyor.
kısa versiyon
App router’da etiketi app/layout.tsx içinde, <body> içine ekleyin:
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>
);
}
YOUR_SITE_KEY yerine paneldeki kurulum sayfasından aldığınız anahtarı yazın. Görünüm panelde durduğunda, ki varsayılan budur, kurulumun tamamı bu. Rehberin geri kalanı daha fazla kontrol istediğiniz durumlar için. Sıfırdan başlıyorsanız, hesap tarafını ana rehber anlatıyor.
neden kök layout
Kök layout her rotayı sarar ve kullanıcı gezinirken bağlı kalır. Başlatıcı kendini sayfaya bir kez bağlar. İstemci tarafı gezinme sayfayı yeniden yüklemez, dolayısıyla başlatıcıyı da yeniden yüklemez ve ziyaretçi sitede dolaşırken bir konuşmayı açık tutabilir.
Etiketi bir sayfa bileşenine koyarsanız sohbet yalnızca o sayfada olur. Birkaç sayfada render edilen bir bileşene koyarsanız, tam yeniden yüklemelerde kopyalar oluşur (başlatıcı kendi ikinci kopyasını yok sayar ama yine de boşa giden bir istektir).
next/script ile
next/script, Next.js’te üçüncü taraf betikleri yüklemenin yoludur ve burada gayet iyi çalışır:
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, başlatıcıyı hidrasyondan hemen sonra yükler. lazyOnload tarayıcının boşa çıkmasını bekler, bu da yavaş telefonlarda düğmenin görünmesini biraz geciktirir. 2 kb’lık bir başlatıcı için ikisi de uygundur; afterInteractive düğmeyi daha erken gösterir. Başlatıcı için beforeInteractive kullanmayın: boşuna kendi kodunuzla yarışır.
görünümü kodda ayarlamak
Panelde yayımladığınız görünüm başlatıcının içinde gelir. Onu depoda tutmayı tercih ederseniz, başlatıcı çalışmadan önce window.Spookat ayarlayın. Sayfanın yapılandırması panelinkinin önüne geçer.
Yükleme sırası önemlidir: başlatıcı çalıştığında yapılandırmanın var olması gerekir. next/script ile yapılandırmaya beforeInteractive (kök layout’ta izinli), başlatıcıya afterInteractive verin:
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 betikleri hidrasyondan önce, afterInteractive olanlar sonra çalışır, bu yüzden sıra garantidir. Anahtarlar her yerdekiyle aynıdır: style (8 taneden biri), font, accent, side, greeting, label, lang. Hepsi dokümanlarda listeleniyor.
pages router
Pages router’da ortak sarmalayıcı pages/_app.tsx dosyasıdır:
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" />
</>
);
}
Bir yapılandırma için, pages router’da beforeInteractive betikleri pages/_document.tsx içine gider. window.Spookat betiğini oraya, <Head> içine, yukarıdakiyle aynı id ve strategy ile koyun ve başlatıcıyı _app.tsx içinde bırakın.
giriş yapmış kullanıcı
pro ve üstünde, sohbete kimin giriş yaptığını söyleyebilirsiniz. Ekibiniz o zaman Slack kartında logged in as 42 görür; webhook’lar, dışa aktarma ve ajanınızın read_convo aracı da kimliği taşır. Kimlik yalnızca sunucunuzun bildiği bir secret ile imzalanır, böylece kimse başkası gibi davranamaz.
Kimlik secret’ını paneldeki kurulum sayfasında oluşturun, bir ortam değişkenine koyun ve sunucuda imzalayın. App router’da kök layout bir sunucu bileşenidir, bu da işi kolaylaştırır:
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>
);
}
İmzayı asla bir istemci bileşeninde hesaplamayın: secret tarayıcıya gider. Eksik ya da yanlış bir imza hata değildir; sohbet sadece kim olduğunu söylemez. Oturumu kök layout’ta okumak her sayfanın istek başına render edilmesine yol açar, bu yüzden layout’unuz bugün statikse eklemeden önce bunu tartın.
content security policy
Uygulamanız bir Content-Security-Policy header’ı gönderiyorsa, widget’ın ihtiyaç duyduğu şunlar:
script-srcvestyle-src:https://cdn.spookat.comfont-src: barındırılan bir font seçtiysenizhttps://cdn.spookat.comconnect-src: tıklamadan sonraki sohbet bağlantısı içinwss://api.spookat.comimg-src: logolar ve ekip arkadaşlarının fotoğrafları içindata:style-src: başlatıcı kendi shadow root’una küçük bir<style>bloğu ekler, bu yüzden onun için satır içi stillere izin verilmelidir
Bunlar olmadan, tarayıcı konsolu hangi direktifin neyi engellediğini tam olarak söyler. Söylediği kaynağı ekleyin ve yeniden yükleyin.
bir production build’de kontrol edin
Geliştirme modu kendi betiklerini ve hot reload’u ekler, bu yüzden gerçek bir build’i kontrol edin:
next build && next start
DevTools açıkken http://localhost:3000 adresini gizli bir pencerede açın. Alan adı önemlidir: sohbet yalnızca kaydettiğiniz alan adından ve alt alan adlarından bağlanır, bu yüzden localhost’ta başlatıcıyı görürsünüz ama sohbet bağlanmaz. Sohbetin kendisini deploy edilmiş sitenizde test edin.
İkisinde de:
- Network, “Disable cache”, yeniden yükleyin,
spookatile filtreleyin: tek istek,cdn.spookat.comadresinden başlatıcı. Kodunuzwindow.Spookatiçinde yayınladığınızdan farklı bir stil ayarlıyorsa iki istek, ikisi decdn.spookat.comadresinden.api.spookat.comadresine hiçbir şey yok. - Application: Spookat’tan çerez yok ve ilk mesaja kadar depolama yok.
- Performance ya da Lighthouse: başlatıcı uzun bir görev olarak görünmemeli. Onunla ve onsuz birkaç çalıştırmayı karşılaştırın; fark, çalıştırmalar arasındaki normal gürültünün içinde kalmalıdır.
- Deploy edilmiş sitede başlatıcıya tıklayın: panel yüklenir, bağlantı açılır, mesajınız Slack’e ulaşır.
deploy hedefleri
Buradaki hiçbir şey uygulamanın nerede çalıştığına bağlı değil. Vercel, bir Node sunucusu, bir container ya da statik bir site için output: "export" aynı HTML’i sunar ve etiket hepsinde aynı şekilde çalışır. Statik bir export’ta, giriş yapmış kullanıcının sizin kontrol ettiğiniz bir sunucudan gelmesi gerekir, çünkü onu imzalayacak bir sunucu render’ı yoktur.
yanıtları bağlayın
Sohbet sitede olduktan sonra panelde Slack’i bağlayın ve bir kanal seçin. Slack rehberi adım adım anlatıyor. Claude Code kullanıyorsanız, Claude Code rehberi yukarıdakilerin hepsini agent’a nasıl yaptıracağınızı gösteriyor.
çok dilli uygulamalar
Uygulamanızda bir [locale] segmenti varsa, widget’a sayfanın hangi dilde olduğunu söyleyin; böylece panel ziyaretçinin tarayıcısına değil sayfaya uyar. Widget 12 dil konuşur; lang, en, pl, de gibi kodlar alır. app/[locale]/layout.tsx içinde:
<Script id="spookat-config" strategy="beforeInteractive">
{`window.Spookat = ${JSON.stringify({ lang: locale })};`}
</Script>
Widget’ta olmayan bir dil, ziyaretçinin tarayıcı diline, sonra İngilizceye düşer. Ekibiniz her durumda sohbette dili görür.
sık yapılan hatalar
- Etiketin
"use client"içeren bir istemci bileşeninde olması. Render edilir ama bileşenle birlikte hidrate olur ve her yeniden bağlanma bir kopya için yeni bir fırsattır. Layout’ta tutun. - Betiği
useEffectiçinde oluşturmak. Aynı sonuç için daha fazla kod veafterInteractive’ten daha geç yüklenir. - Etikette
type="module". Başlatıcı klasik bir betiktir; type eklemeyin. - Paneli önceden yüklemek. Panel betiği için bir
<link rel="preload">, indirmeyi her ziyaretçi için tıklamanın önüne taşır. Bütün mesele beklemesidir. - Sohbeti bir önizleme deploy’unda test etmek. Önizleme URL’leri başka alan adlarıdır. Başlatıcı görünür, sohbet bağlanmaz. Beklenen budur.
bundle’ınıza ne ekler
Hiçbir şey. Başlatıcı CDN’imizden ayrı bir dosya olarak gelir, yani JavaScript bundle’ınızın parçası değildir ve next build komutunun yazdırdığı “First Load JS” rakamları tamamen aynı kalır. React’e de dokunmaz: düğme kendi shadow root’unda yaşar, bu yüzden sizin CSS’iniz onu bozamaz, onun CSS’i de uygulamanıza sızamaz. Build’inizin bilmesi gereken tek şey o etikettir.
kontrol listesi
- Kök layout’ta tek bir etiket ya da
<Script>(app/layout.tsxya dapages/_app.tsx). - Varsa yapılandırma başlatıcıdan önce çalışıyor.
- Kullanıcı imzaları yalnızca sunucuda hesaplanıyor.
- CSP’niz varsa, CDN’e ve sohbet bağlantısına izin veriyor.
- Bir production build’de kontrol edildi: tıklamadan önce tek istek (kodunuz yayınlamadığınız bir stil ayarlıyorsa iki, ikisi de
cdn.spookat.comadresinden), hiç çerez yok.
starter ile site başına aylık $4.99 tutar ve deneme süresi kartsız 14 gündür.