pregunta respondida, venta salvadael chat no necesita banner de cookiescompañeros ilimitadosresponde desde slack o nuestra app

/blog / guías

añade chat en vivo a una web con next.js

· por kuba · 6 min de lectura

Un enchufe con la etiqueta next.js a punto de entrar en una toma rosa, con el gato de Spookat montado en el cable.

El chat en vivo en Next.js es una etiqueta script en el layout raíz. Esta guía muestra dónde va con el app router y con el pages router, cómo pasar el aspecto del widget y el usuario con sesión iniciada sin romper el orden de carga, qué añadir a una Content Security Policy y cómo comprobar el resultado en un build de producción.

la versión corta

Con el app router, añade la etiqueta en 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>
  );
}

Sustituye YOUR_SITE_KEY por la clave de la página de instalación del panel. Esa es toda la instalación cuando el aspecto vive en el panel, que es lo normal. El resto de la guía es para cuando quieres más control. Si empiezas de cero, la guía principal cubre la parte de la cuenta.

por qué el layout raíz

El layout raíz envuelve cada ruta y se mantiene montado mientras el usuario navega. El lanzador se engancha a la página una sola vez. La navegación del lado del cliente no recarga la página, así que tampoco recarga el lanzador, y un visitante puede mantener una conversación abierta mientras hace clic por ahí.

Si pones la etiqueta en un componente de página, el chat solo existe en esa página. Si la pones en un componente que se renderiza en varias páginas, tendrás duplicados en las recargas completas (el lanzador ignora su propia segunda copia, pero sigue siendo una petición desperdiciada).

con next/script

next/script es la forma de Next.js de cargar scripts de terceros, y aquí funciona bien:

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 carga el lanzador justo después de la hidratación. lazyOnload espera a que el navegador esté inactivo, lo que retrasa un poco la aparición del botón en móviles lentos. Cualquiera de los dos va bien para un lanzador de 2 kb; afterInteractive muestra el botón antes. No uses beforeInteractive para el lanzador: competiría con tu propio código para nada.

definir el aspecto en el código

El aspecto que publicas en el panel viaja dentro del lanzador. Si prefieres tenerlo en el repositorio, define window.Spookat antes de que se ejecute el lanzador. La configuración de la página tiene prioridad sobre la del panel.

El orden de carga importa: la configuración tiene que existir cuando el lanzador se ejecuta. Con next/script, dale beforeInteractive a la configuración (permitido en el layout raíz) y afterInteractive al lanzador:

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

Los scripts beforeInteractive se ejecutan antes de la hidratación y los afterInteractive después, así que el orden está garantizado. Las claves son las mismas que en cualquier otro sitio: style (uno de 8), font, accent, side, greeting, label, lang. La documentación las lista todas.

el pages router

Con el pages router, el envoltorio compartido es 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 una configuración, en el pages router los scripts beforeInteractive van en pages/_document.tsx. Pon ahí el script de window.Spookat, dentro de <Head>, con el mismo id y la misma strategy que arriba, y deja el lanzador en _app.tsx.

el usuario con sesión iniciada

Desde pro, puedes decirle al chat quién ha iniciado sesión. Tu equipo verá entonces logged in as 42 en la tarjeta de Slack, y los webhooks, la exportación y el read_convo de tu agente llevan el id. El id se firma con un secreto que solo conoce tu servidor, así que nadie puede hacerse pasar por otra persona.

Crea el secreto de identidad en la página de instalación del panel, guárdalo en una variable de entorno y firma en el servidor. Con el app router, el layout raíz es un componente de servidor, lo que lo hace sencillo:

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 calcules la firma en un componente de cliente: el secreto acabaría en el navegador. Una firma que falta o es incorrecta no es un error; el chat simplemente no dice quién es. Leer la sesión en el layout raíz hace que cada página se renderice por petición, así que si tu layout hoy es estático, valóralo antes de añadirlo.

content security policy

Si tu app envía una cabecera Content-Security-Policy, el widget necesita:

  • script-src y style-src: https://cdn.spookat.com
  • font-src: https://cdn.spookat.com, si elegiste una fuente alojada
  • connect-src: wss://api.spookat.com, para la conexión del chat después de un clic
  • img-src: data:, para los logos y las fotos de tu equipo
  • style-src: el lanzador inyecta un pequeño bloque <style> en su propio shadow root, así que hay que permitir estilos inline para él

Sin esto, la consola del navegador dice exactamente qué directiva bloqueó qué. Añade la fuente que indica y recarga.

compruébalo en un build de producción

El modo desarrollo añade sus propios scripts y la recarga en caliente, así que comprueba un build real:

next build && next start

Abre http://localhost:3000 en una ventana privada con DevTools abierto. El dominio importa: el chat solo se conecta desde el dominio que registraste y sus subdominios, así que en localhost verás el lanzador, pero el chat no se conectará. Prueba el chat en sí en tu web desplegada.

En cualquiera de los dos:

  1. Network, “Disable cache”, recarga, filtra por spookat: una petición, el lanzador desde cdn.spookat.com. Dos, ambas desde cdn.spookat.com, si tu código pone en window.Spookat un estilo distinto del que publicaste. Nada hacia api.spookat.com.
  2. Application: ninguna cookie de Spookat y ningún almacenamiento hasta un primer mensaje.
  3. Performance o Lighthouse: el lanzador no debería aparecer como una tarea larga. Compara varias ejecuciones con y sin él; la diferencia debería quedar dentro del ruido normal entre ejecuciones.
  4. Haz clic en el lanzador en la web desplegada: el panel se carga, la conexión se abre y tu mensaje llega a Slack.

destinos de despliegue

Nada de esto depende de dónde se ejecute la app. Vercel, un servidor Node, un contenedor u output: "export" para una web estática sirven el mismo HTML, y la etiqueta funciona igual en todos. En una exportación estática, el usuario con sesión iniciada tiene que venir de un servidor que controles, porque no hay renderizado en servidor que lo firme.

conecta las respuestas

Cuando el chat esté en la web, conecta Slack en el panel y elige un canal. La guía de Slack te acompaña. Si usas Claude Code, la guía de Claude Code muestra cómo dejar que el agente haga todo lo anterior.

apps multilingües

Si tu app tiene un segmento [locale], dile al widget en qué idioma está la página, para que el panel coincida con la página y no con el navegador del visitante. El widget habla 12 idiomas; lang acepta códigos como en, pl, de. En app/[locale]/layout.tsx:

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

Un idioma que el widget no tiene recurre al del navegador del visitante y luego al inglés. Tu equipo ve el idioma en el chat en cualquier caso.

errores frecuentes

  • La etiqueta dentro de un componente de cliente con "use client". Se renderiza, pero se hidrata con el componente, y cada remontaje es otra ocasión para un duplicado. Déjala en el layout.
  • Crear el script en useEffect. Más código para el mismo resultado, y carga más tarde que afterInteractive.
  • type="module" en la etiqueta. El lanzador es un script clásico; no pongas el tipo.
  • Precargar el panel. Un <link rel="preload"> para el script del panel adelanta su descarga antes del clic para cada visitante. La gracia es que espere.
  • Probar el chat en un despliegue de preview. Las URLs de preview son otros dominios. El lanzador aparece, el chat no se conecta. Es lo esperado.

lo que añade a tu bundle

Nada. El lanzador viene de nuestra CDN como archivo propio, así que no forma parte de tu bundle de JavaScript, y las cifras de “First Load JS” que imprime next build se quedan exactamente igual. Tampoco toca React: el botón vive en su propio shadow root, así que tu CSS no puede romperlo y su CSS no puede filtrarse a tu app. Lo único que tu build necesita saber es la etiqueta.

checklist

  • Una etiqueta o un <Script> en el layout raíz (app/layout.tsx o pages/_app.tsx).
  • La configuración, si la hay, se ejecuta antes del lanzador.
  • Las firmas de usuario se calculan solo en el servidor.
  • La CSP permite la CDN y la conexión del chat, si tienes CSP.
  • Comprobado en un build de producción: una petición antes de un clic (dos, ambas desde cdn.spookat.com, si tu código pone un estilo que no publicaste), sin cookies.

Cuesta $4.99 al mes por sitio con starter, y la prueba dura 14 días sin tarjeta.

¿te gustó? apúntate a la lista.

las invitaciones salen en tandas pequeñas. cuando llegue la tuya: 14 días gratis, sin tarjeta.

solo te escribimos por tu invitación y el lanzamiento. puedes darte de baja cuando quieras. privacidad