Expón chat del sitio desde un host Next.js sin poner una clave API en el navegador. Usa @mailoo/chat para rutas BFF y el widget flotante opcional. Contrato de la API y notas de WebSocket: chat-widget{.interpreted-text role="doc"}, chat-websocket-production{.interpreted-text role="doc"}.
Flujo
- El navegador llama solo a rutas del mismo origen en tu aplicación Next.js:
POST /api/v1/webhooks/chat/submit--- cuerpo{ "content": "...", "sessionPublicId?": "..." }GET /api/v1/webhooks/chat/messages?sessionPublicId=...--- polling opcional (sincesoportado)GET /api/v1/webhooks/chat/status--- estado opcional de la integración
- El controlador de ruta reenvía a Mailoo con
X-API-Keydel entorno del servidor (createChatSubmitHandler/createChatMessagesHandler/createChatStatusHandlerde@mailoo/chat/routes). Opcionalmente inyectasenderEmail/senderNamemedianteresolveSenderpara que el panel muestre la identidad del visitante en lugar deGuest. - Opcional: el widget abre WebSocket hacia la API. El host se resuelve en tiempo de ejecución en el servidor (sin
NEXT_PUBLIC_*para el origen de la API):
- Orden por defecto:
MAILOO_CHAT_WS_ORIGIN→MAILOO_CHAT_INTEGRATION_API→API_BASE_URL(getMailooChatWebSocketOriginde@mailoo/chat) - Establece
MAILOO_CHAT_WS_ORIGINcuando el nombre de host de la API visible para el navegador difiere de la URL que usa el servidor Node para llamar a la API
Superficie del paquete
// app/api/v1/webhooks/chat/submit/route.ts
import { createChatSubmitHandler } from '@mailoo/chat/routes'
export const POST = createChatSubmitHandler({
resolveSender: async () => {
const session = await auth()
return session?.user?.email
? { email: session.user.email, name: session.user.name ?? undefined }
: null
},
})
// app/api/v1/webhooks/chat/messages/route.ts
import { createChatMessagesHandler } from '@mailoo/chat/routes'
export const GET = createChatMessagesHandler()
// app/api/v1/webhooks/chat/status/route.ts
import { createChatStatusHandler } from '@mailoo/chat/routes'
export const GET = createChatStatusHandler()
Sesión headless (UI personalizada) mediante @mailoo/chat/hooks:
import { useMailooChatSession } from '@mailoo/chat/hooks'
const chat = useMailooChatSession({
apiOrigin,
projectUid,
integrationId,
isAuthenticated,
enabled: panelOpen,
})
Variables de entorno (solo servidor)
MAILOO_CHAT_INTEGRATION_API=https://api.mailoo.app
MAILOO_CHAT_INTEGRATION_API_KEY=your-api-key-here
MAILOO_CHAT_INTEGRATION_PROJECT_UID=your-project-uid
MAILOO_CHAT_INTEGRATION_ID=your-jsbox-integration-id
MAILOO_CHAT_WELCOME_MESSAGE=Hello! How can we help?
# Origen público opcional de la API para ws/wss cuando difiere de la URL de la API del servidor
# MAILOO_CHAT_WS_ORIGIN=https://api.mailoo.app
La clave API necesita chat.send-message (o FULL). Copia projectUid e integrationId desde Conexión y ajustes / fragmento de integración del panel de control.
Widget flotante
Importa el CSS del shell (obligatorio para que el FAB quede fijo aunque Tailwind no escanee @mailoo/chat). En este monorepo PostCSS resuelve mejor una ruta relativa:
@import '../../../../packages/chat/src/styles/widget.css';
/* paquete publicado: @import '@mailoo/chat/styles'; */
Recomendado (ruta por defecto): resuelve la config en el servidor y monta MailooSiteChatShell. Marca, temas, posición, bienvenida, privacidad y operadores se cargan siempre desde JSBOX config.chatWidget:
import { resolveMailooChatHostConfig } from '@mailoo/chat'
import { MailooSiteChatShell } from '@mailoo/chat/client'
const chat = await resolveMailooChatHostConfig({
locale,
isAuthenticated: Boolean(session?.user?.email),
})
{chat ? (
<MailooSiteChatShell
chat={chat}
unavailableAction={{
type: 'mailto',
email: 'support@example.com',
}}
/>
) : null}
resolveMailooChatHostConfig devuelve null si falta env MAILOO_CHAT_* o no se puede cargar widget-config (errores HTTP incluido 429, red, payload inválido). Nunca lanza --- la página sigue HTTP 200 y omite el shell del chat. Llama openMailooSiteChat() desde cualquier componente cliente para abrir el panel.
UI personalizada: pasa widgetConfig a MailooSiteChatWidget tú mismo (desde fetchMailooChatWidgetConfig o BFF GET …/webhooks/chat/widget-config). El launcher es un FAB fijo (esquinas o middle-left / middle-right) con Heroicons.
Chat no disponible / límite de peticiones
El paquete clasifica fallos de transporte y servicio al enviar o sondear mensajes (no muestra el texto crudo de la API en la UI):
rate_limited--- HTTP 429 (límite por minuto de la clave API)unavailable--- red, BFF 502/**503** u otros 5xx
Errores de validación, sesión cerrada (409) y sesión inexistente (404, se limpia sola) siguen siendo errores inline --- no abren el panel de indisponibilidad.
resolveMailooChatHostConfig devuelve null (no montes el chat) si no carga widget-config. Así la página del host sigue arriba; el panel de indisponibilidad solo aplica después de montar el widget, cuando falla send/poll.
El integrador elige la acción (correo, página de soporte o UI propia):
<MailooSiteChatShell
chat={chat}
unavailableAction={{ type: 'mailto', email: 'support@example.com' }}
// o: { type: 'link', href: '/support', label: 'Contactar soporte' }
// o: renderUnavailable={({ fault, labels, retry }) => ...}
/>
Sin unavailableAction, el visitante ve la explicación y Reintentar (borra el fault y reanuda el polling). Conviene pasar un contacto. Headless: fault / onFault / retryAfterFault en useMailooChatSession.
Dogfood de mailoo.app: el sitio monta MailooSiteChatShell en cada layout de locale cuando están definidas MAILOO_CHAT_*, con unavailableAction → mailto:support@mailoo.app. La tarjeta Soporte → Chat en vivo abre el mismo panel. Sin esas variables la tarjeta queda en «Próximamente» y no se muestra el launcher.
Consola de chat del panel de control
Para integraciones JSBOX, Abrir consola de chat abre la UI del operador para esa integración. Los operadores usan la sesión del panel; los visitantes usan el webhook público y las rutas WebSocket de arriba.
Relacionado
chat-widget{.interpreted-text role="doc"} --- webhooks públicos y alcanceschat-websocket-production{.interpreted-text role="doc"} --- notas de WebSocket en producciónnextjs-packages{.interpreted-text role="doc"} --- resumen de paquetes