pergunta respondida, venda salvao chat dispensa banner de cookiestime ilimitadoresponda pelo slack ou nosso app

/blog / guias

coloque chat ao vivo num site next.js

· por kuba · 6 min de leitura

Um plugue com a etiqueta next.js prestes a entrar numa tomada rosa, com o gato do Spookat montado no cabo.

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

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

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:

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 lista todas.

o pages router

No pages router, o wrapper compartilhado é 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.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:

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:

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 mostra o passo a passo. Se você usa o Claude Code, o guia do Claude Code 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:

<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.

curtiu? entre na lista.

os convites saem em pequenos lotes. quando o seu chegar: 14 dias grátis, sem cartão.

só escrevemos sobre o seu convite e o lançamento. dá pra parar quando quiser. privacidade