# bir next.js sitesine canlı sohbet ekleyin

Spookat sohbetini app router ya da pages router kullanan bir Next.js uygulamasına görünümüyle ve isteğe bağlı giriş yapmış kullanıcıyla ekleyin, tıklamadan önce API'mize hiç istek gitmediğini doğrulayın.

2026-11-04 · Kuba

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:

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

`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](/tr/blog/add-live-chat-to-your-website/) 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:

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

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

`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](/tr/docs/) listeleniyor.

## pages router

Pages router'da ortak sarmalayıcı `pages/_app.tsx` dosyasıdır:

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

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:

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

İ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-src` ve `style-src`: `https://cdn.spookat.com`
- `font-src`: barındırılan bir font seçtiyseniz `https://cdn.spookat.com`
- `connect-src`: tıklamadan sonraki sohbet bağlantısı için `wss://api.spookat.com`
- `img-src`: logolar ve ekip arkadaşlarının fotoğrafları için `data:`
- `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:

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

1. **Network**, "Disable cache", yeniden yükleyin, `spookat` ile filtreleyin: tek istek, `cdn.spookat.com` adresinden başlatıcı. Kodunuz `window.Spookat` içinde yayınladığınızdan farklı bir stil ayarlıyorsa iki istek, ikisi de `cdn.spookat.com` adresinden. `api.spookat.com` adresine hiçbir şey yok.
2. **Application**: Spookat'tan çerez yok ve ilk mesaja kadar depolama yok.
3. **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.
4. 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](/tr/blog/answer-website-chat-from-slack/) adım adım anlatıyor. Claude Code kullanıyorsanız, [Claude Code rehberi](/tr/blog/claude-code-live-chat/) 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:

```tsx
<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 `useEffect` içinde oluşturmak.** Aynı sonuç için daha fazla kod ve `afterInteractive`'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.tsx` ya da `pages/_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.com` adresinden), hiç çerez yok.

starter ile site başına aylık $4.99 tutar ve deneme süresi kartsız 14 gündür.

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