# coloque chat ao vivo num site next.js

Adicione o chat do Spookat a um app Next.js, no app router ou no pages router, com a aparência e, se quiser, o usuário logado, e confira que nenhuma requisição vai à nossa API antes de um clique.

2026-11-04 · Kuba

Chat ao vivo no Next.js é uma tag script no layout raiz. Este guia mostra onde ela vai no app router e no pages router, como passar a aparência do widget e o usuário logado sem bagunçar a ordem de carregamento, o que adicionar numa Content Security Policy e como conferir o resultado num build de produção.

## a versão curta

No app router, adicione a tag em `app/layout.tsx`, dentro de `<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>
  );
}
```

Troque `YOUR_SITE_KEY` pela chave da página de instalação do painel. Essa é a instalação inteira quando a aparência fica no painel, que é o padrão. O resto deste guia é para quando você quer mais controle. Se está começando do zero, o [guia principal](/pt/blog/add-live-chat-to-your-website/) cobre a parte da conta.

## por que o layout raiz

O layout raiz envolve todas as rotas e continua montado enquanto o usuário navega. O botão do chat se prende à página uma vez só. A navegação do lado do cliente não recarrega a página, então também não recarrega o botão, e um visitante pode manter a conversa aberta enquanto clica pelo site.

Coloque a tag num componente de página e o chat só existe naquela página. Coloque num componente que renderiza em várias páginas e você ganha duplicatas nos recarregamentos completos (o botão ignora a própria segunda cópia, mas ainda é uma requisição desperdiçada).

## com next/script

`next/script` é o jeito Next.js de carregar scripts de terceiros, e funciona bem aqui:

```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` carrega o botão logo depois da hidratação. `lazyOnload` espera o navegador ficar ocioso, o que atrasa um pouco o aparecimento do botão em celulares lentos. Os dois servem para um botão de 2 kb; `afterInteractive` mostra o botão mais cedo. Não use `beforeInteractive` para o botão: ele disputaria espaço com o seu próprio código à toa.

## definindo a aparência no código

A aparência que você publica no painel vai dentro do botão. Se prefere manter no repositório, defina `window.Spookat` antes de o botão rodar. A configuração da página vale mais que a do painel.

A ordem de carregamento importa: a configuração precisa existir quando o botão executar. Com `next/script`, dê `beforeInteractive` para a configuração (permitido no layout raiz) e `afterInteractive` para o botão:

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

Scripts `beforeInteractive` rodam antes da hidratação e os `afterInteractive` depois, então a ordem é garantida. As chaves são as mesmas de qualquer outro lugar: `style` (um de 8), `font`, `accent`, `side`, `greeting`, `label`, `lang`. A [documentação](/pt/docs/) lista todas.

## o pages router

No pages router, o wrapper compartilhado é `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" />
    </>
  );
}
```

Para uma configuração, no pages router os scripts `beforeInteractive` ficam em `pages/_document.tsx`. Coloque o script do `window.Spookat` lá, dentro de `<Head>`, com o mesmo `id` e a mesma `strategy` de cima, e deixe o botão no `_app.tsx`.

## o usuário logado

A partir do pro, você pode dizer ao chat quem está logado. Sua equipe passa a ver `logged in as 42` no card do Slack, e os webhooks, a exportação e o `read_convo` do seu agente levam o id. O id é assinado com um segredo que só o seu servidor conhece, então ninguém consegue se passar por outra pessoa.

Crie o segredo de identidade na página de instalação do painel, coloque numa variável de ambiente e assine no servidor. No app router, o layout raiz é um componente de servidor, o que deixa isso simples:

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

Nunca calcule a assinatura num componente de cliente: o segredo iria parar no navegador. Uma assinatura ausente ou errada não é um erro; o chat simplesmente não diz quem é. Ler a sessão no layout raiz faz toda página renderizar por requisição, então, se o seu layout é estático hoje, pense nisso antes de adicionar.

## content security policy

Se o seu app manda um cabeçalho `Content-Security-Policy`, o widget precisa de:

- `script-src` e `style-src`: `https://cdn.spookat.com`
- `font-src`: `https://cdn.spookat.com`, se você escolheu uma fonte hospedada
- `connect-src`: `wss://api.spookat.com`, para a conexão do chat depois de um clique
- `img-src`: `data:`, para os logos e as fotos da equipe
- `style-src`: o botão injeta um pequeno bloco `<style>` no próprio shadow root, então estilos inline precisam ser permitidos para ele

Sem isso, o console do navegador diz exatamente qual diretiva bloqueou o quê. Adicione a origem que ele indicar e recarregue.

## confira num build de produção

O modo de desenvolvimento adiciona scripts próprios e hot reload, então confira um build de verdade:

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

Abra `http://localhost:3000` numa janela anônima com o DevTools aberto. O domínio importa: o chat só conecta a partir do domínio que você registrou e dos subdomínios dele, então no localhost você vê o botão, mas o chat não conecta. Teste o chat em si no seu site publicado.

Em qualquer um dos dois:

1. **Network**, "Disable cache", recarregue, filtre por `spookat`: uma requisição, o botão vindo de `cdn.spookat.com`. Duas, ambas de `cdn.spookat.com`, se o seu código define em `window.Spookat` um estilo diferente do que você publicou. Nada para `api.spookat.com`.
2. **Application**: nenhum cookie do Spookat, e nenhum armazenamento até a primeira mensagem.
3. **Performance ou Lighthouse**: o botão não deve aparecer como uma tarefa longa. Compare algumas execuções com e sem ele; a diferença deve ficar dentro do ruído normal entre execuções.
4. **Clique no botão do chat** no site publicado: o painel carrega, a conexão abre, sua mensagem chega no Slack.

## destinos de deploy

Nada aqui depende de onde o app roda. Vercel, um servidor Node, um container ou `output: "export"` para um site estático servem o mesmo HTML, e a tag funciona igual em todos. Num export estático, o usuário logado precisa vir de um servidor que você controla, já que não há renderização no servidor para assinar.

## conecte as respostas

Com o chat no site, conecte o Slack no painel e escolha um canal. O [guia do Slack](/pt/blog/answer-website-chat-from-slack/) mostra o passo a passo. Se você usa o Claude Code, o [guia do Claude Code](/pt/blog/claude-code-live-chat/) mostra como deixar o agente fazer tudo isso.

## apps multilíngues

Se o seu app tem um segmento `[locale]`, diga ao widget em que idioma a página está, para o painel combinar com a página e não com o navegador do visitante. O widget fala 12 idiomas; `lang` aceita códigos como `en`, `pl`, `de`. Em `app/[locale]/layout.tsx`:

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

Um idioma que o widget não tem cai para o do navegador do visitante e depois para o inglês. Sua equipe vê o idioma no chat de qualquer jeito.

## erros comuns

- **A tag dentro de um componente de cliente com `"use client"`.** Ela renderiza, mas é hidratada junto com o componente, e cada remontagem é mais uma chance de duplicata. Deixe no layout.
- **Criar o script num `useEffect`.** Mais código para o mesmo resultado, e carrega depois do `afterInteractive`.
- **`type="module"` na tag.** O botão é um script clássico; deixe o tipo de fora.
- **Pré-carregar o painel.** Um `<link rel="preload">` para o script do painel joga o download dele para antes do clique, para cada visitante. A graça é justamente ele esperar.
- **Testar o chat num deploy de preview.** URLs de preview são outros domínios. O botão aparece, o chat não conecta. É o esperado.

## o que isso adiciona ao seu bundle

Nada. O botão vem da nossa CDN como um arquivo separado, então não faz parte do seu bundle JavaScript, e os números de "First Load JS" que o `next build` mostra continuam exatamente iguais. Ele também não mexe no React: o botão vive no próprio shadow root, então seu CSS não consegue quebrá-lo e o CSS dele não vaza para o seu app. A única coisa que seu build precisa saber é a tag.

## checklist

- Uma tag ou `<Script>` no layout raiz (`app/layout.tsx` ou `pages/_app.tsx`).
- A configuração, se houver, roda antes do botão.
- As assinaturas de usuário são calculadas só no servidor.
- A CSP libera a CDN e a conexão do chat, se você tem uma CSP.
- Conferido num build de produção: uma requisição antes de um clique (duas, ambas de `cdn.spookat.com`, se o seu código define um estilo que você não publicou), nenhum cookie.

Custa $4.99 por mês por site no starter, e o teste dura 14 dias sem cartão.

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