Instalación
Instalar el widget toma dos pasos: cargar el script y luego iniciarlo con el slug de tu espacio. Chat, ideas, roadmap y novedades quedan dentro de tu app; carga en segundo plano y vive en Shadow DOM, así que no toca tus estilos.
Con el lanzador por defecto, eso basta para que aparezca el botón flotante en tu página.
1. Agrega el script
Pega esto cerca del final de tu página, justo antes de cerrar </body>:
<script> window.Jotly = window.Jotly || function () { (Jotly.q = Jotly.q || []).push(arguments) };</script><script async src="https://jotly.si/widget.js"></script>La primera línea crea la función global window.Jotly y guarda en una cola las llamadas que hagas antes de que cargue widget.js; al cargar, las ejecuta en orden.
2. Inicia el widget
Después del script, inicia Jotly con tu slug:
<script> Jotly('init', 'TU_ESPACIO');</script>Cambia TU_ESPACIO por el slug de tu espacio. Lo ves en la dirección de tu panel (/app/TU_ESPACIO) y en Ajustes → Instalar widget, donde el código ya viene con tu slug. Con el mismo slug se arma tu portal público en /b/TU_ESPACIO.
Ejemplo mínimo
Ésta es la instalación completa más corta:
<script> window.Jotly = window.Jotly || function () { (Jotly.q = Jotly.q || []).push(arguments) }; Jotly('init', 'TU_ESPACIO'); // Opcional: identifica a tu usuario Jotly('identify', { user: { id: 'user_123', email: 'ana@empresa.com', firstName: 'Ana' } });</script><script async src="https://jotly.si/widget.js"></script>Una sola etiqueta (sin script en línea)
Si tu política de seguridad de contenido (CSP) no permite scripts en línea, basta con una etiqueta. El widget se inicia solo con data-project; si además llamas a Jotly('init', …), tu llamada gana sobre el atributo.
<script async src="https://jotly.si/widget.js" data-project="TU_ESPACIO"></script>Opcional: identifica a tus usuarios
Si tu producto ya sabe quién es el usuario, identifícalo después de iniciar:
const { viewer, identityTrust, warnings } = await Jotly.identify({ user: { id: 'user_123', // obligatorio (o un token firmado) email: 'ana@empresa.com', firstName: 'Ana', lastName: 'López', avatar: 'https://…/ana.png', traits: { plan: 'pro', monthlySpend: 499 }, }, companies: [{ id: 'acme', name: 'Acme SA' }],});Hace falta user.id o un token firmado en tu servidor. La misma entrada con la sesión viva no hace nada y, si llamas varias veces, gana la última. Para que nadie pueda hacerse pasar por otro usuario, firma la identidad en tu servidor como se explica en Identificar usuarios. El formato anterior ({ id, email, name, signature }) sigue funcionando.
Opciones de init
Las opciones del widget también se configuran desde Ajustes → Instalar widget; si las pasas en init, mandan las tuyas. init devuelve una promesa con el estado y sólo rechaza por errores de programación (INVALID_PROJECT_ID, INVALID_OPTIONS).
Jotly('init', 'TU_ESPACIO', { widget: { // false lo desactiva (sigues teniendo createEmbed e identify) launcher: true, // false, o { selector: '#ayuda' } para usar tu propio botón whispers: true, // avisos efímeros junto al lanzador notifications: { updates: 'whisper' }, // 'whisper' | 'full' | 'none' position: 'right', // 'right' | 'left' offset: { bottom: 20, side: 20 }, // px theme: 'auto', // 'auto' | 'light' | 'dark' }, locale: 'es-MX', // por defecto, el idioma del navegador pageContext: 'origin', // 'origin' | 'path' | 'none': cuánto de la URL ve tu equipo en el chat onReady: function () {}, onError: function (error) {}, // { code, message, recoverable }});Con notifications.updates: 'full', una novedad nueva abre el widget en el artículo como máximo una vez por carga, sólo con la pestaña visible y con foco y el widget cerrado. Con 'whisper' aparece un aviso junto al lanzador que dura 7 segundos.
API en el navegador
Cada método se llama igual como Jotly('open', { to: 'roadmap' }) o, ya cargado el script, como Jotly.open({ to: 'roadmap' }). Las llamadas que hagas antes de que cargue widget.js se guardan en una cola y se ejecutan en orden.
// Promesa con el estado al terminar de iniciar (no rechaza por red: revisa state.status)const state = await Jotly.ready(); // Abrir en un punto exacto; resuelve cuando el panel está visibleJotly.open({ to: 'home' });Jotly.open({ to: 'roadmap' });Jotly.open({ to: 'notifications' });Jotly.open({ to: 'feedback' });Jotly.open({ to: 'feedback', compose: true }); // nueva solicitudJotly.open({ to: 'feedback', submissionId: 'pst_123' }); // una solicitudJotly.open({ to: 'updates', updateId: 'chg_123' }); // una novedadJotly.open({ to: 'messages', compose: { text: 'Hola, necesito ayuda con…' } }); Jotly.close();Jotly.toggle(); // Apariencia e idiomaJotly.setTheme('dark'); // 'auto' | 'light' | 'dark'await Jotly.setLocale('es-MX'); // Estado de sólo lecturaJotly.getState(); // Al cerrar sesión en tu appawait Jotly.logout(); // Quitar todo; en kioscos borra también conversaciones anónimas, borradores y contacto recordadoJotly.destroy({ clearConversationData: true });Eventos
Jotly.on(evento, función) devuelve la función para dejar de escuchar. Los listeners sobreviven a destroy().
| Evento | Recibe |
|---|---|
ready | El estado. Si ya estaba listo, se llama de inmediato. |
statechange | El estado, en cada cambio. |
open | El estado al abrir el panel. |
close | El estado al cerrar el panel. |
identify | { viewer, identityTrust } |
unread | { count } |
presence | { availability: 'online' | 'neutral' } |
error | { code, message, recoverable } |
destroy | El estado tras destroy(). |
const off = Jotly.on('unread', function ({ count }) { document.querySelector('#ayuda-badge').textContent = count || '';});off(); // deja de escuchar Jotly.on('error', function (error) { if (!error.recoverable) console.error(error.code, error.message);});Escuchar unread o presence mantiene el conteo al día aunque uses widget: false.
Estado
{ status: 'ready', // 'idle' | 'initializing' | 'ready' | 'error' | 'destroyed' projectId: 'TU_ESPACIO', locale: 'es-MX', unreadCount: 1, // respuestas del chat + novedades sin ver; null al inicio presence: 'online', // 'online' | 'neutral' ('neutral' no significa desconectado) lastError: null, widget: { enabled: true, open: false, position: 'right', theme: 'auto' }, session: { status: 'anonymous', identityTrust: null, viewer: null },}Errores
Todos los errores tienen la forma { code, message, recoverable }. Llegan al evento error, a onError y a state.lastError; las promesas rechazan con el mismo objeto.
| Código | Cuándo |
|---|---|
INVALID_PROJECT_ID | El id del proyecto no tiene formato válido o el espacio no existe. |
INVALID_OPTIONS | Alguna opción de init, setTheme, setLocale o createEmbed no es válida. |
INVALID_NAVIGATION | El destino de open o la ruta de navigate no son válidos. |
INVALID_IDENTIFY | identify sin token ni user.id. |
IDENTIFY_FAILED | El servidor rechazó la identidad (firma inválida, espacio que exige firma, etc.). Es el único error de red que rechaza la promesa. |
BOOTSTRAP_FAILED | No se pudo cargar la configuración; el estado queda en error. |
NOT_READY | Llamaste un método después de que init falló. |
WIDGET_DISABLED | open con widget: false. |
CONVERSATIONS_DISABLED | open({ to: 'messages' }) con los mensajes apagados; se abre Inicio. |
LAUNCHER_NOT_FOUND | Ningún elemento coincide con launcher.selector (se sigue escuchando por si aparece). |
EMBED_CONTAINER_NOT_FOUND | No existe el contenedor del embebido. |
DESTROYED | Llamaste un método después de destroy; vuelve a llamar init. |
Embebidos en tu página
Jotly.createEmbed muestra el portal (feedback, roadmap y novedades) dentro de tu app en un iframe, sin su barra de navegación y con la misma identidad que el widget. Puedes tener varios a la vez.
<div data-jotly-embed style="height: 720px"></div><script> Jotly('init', 'TU_ESPACIO', { widget: false }); var portal = Jotly.createEmbed({ // container: '#feedback', // por defecto [data-jotly-embed] path: '/roadmap', // por defecto '/' basePath: '/feedback', // sincroniza la URL: /feedback/p/<slug> theme: 'auto', onReady: function () {}, onError: function (error) {}, }); portal.navigate('/updates'); // portal.destroy();</script>- Con
basePath, cada cambio de página dentro del embebido actualiza la URL de tu sitio conhistory, y los botones atrás y adelante del navegador mueven el embebido. Al cargar una URL bajobasePath, el embebido abre esa misma página. - El iframe y tu página sólo se hablan con mensajes
jotly:(ready,route:change,navigate,identify,logout) y cada lado verifica la ventana y el origen del otro. - También puedes embeber el portal a mano: cualquier página de
/b/TU_ESPACIOacepta?navbar=hiddeny?theme=light|dark|auto.
Next.js
Con next/script lo cargas una vez para todo el sitio y llamas a window.Jotly desde cualquier componente.
// app/layout.tsximport Script from "next/script"; export default function RootLayout({ children }) { return ( <html lang="es"> <body> {children} <Script id="jotly-init" strategy="afterInteractive"> {`window.Jotly = window.Jotly || function () { (Jotly.q = Jotly.q || []).push(arguments) };Jotly('init', 'TU_ESPACIO');`} </Script> <Script src="https://jotly.si/widget.js" strategy="afterInteractive" /> </body> </html> );}Verifica la instalación
Al recargar la página:
- El lanzador del widget aparece en la esquina inferior.
- Al hacer clic se abre el widget.
- Puedes moverte entre mensajes, ideas, roadmap y novedades dentro del widget.
- En la consola,
Jotly.getState().statusdice'ready'.
Si usas tu propio botón (launcher: { selector } o launcher: false), el widget puede estar bien instalado aunque no aparezca ningún botón flotante.
Problemas comunes
El widget no aparece
Revisa primero:
- El script de
widget.jsestá en la página Jotly('init', …)corre con el slug correcto (o la etiqueta traedata-project)launcherno está enfalseywidgetno está enfalseJotly.getState().lastErrorno trae un error
El widget aparece, pero no reconoce al usuario
Casi siempre significa que la instalación está bien pero falta la identidad. Empieza por Identificar usuarios.
Si mandas una firma que no coincide, Jotly rechaza la identificación con IDENTIFY_FAILED en vez de crear un usuario sin verificar.
Ver también Widget de feedback en tu app