13 — Widget embebido (Help Center flotante)¶
El widget es lo que el cliente del SaaS pega en su web para mostrar el centro de ayuda. Es el "producto demo" del SaaS framework.
Snippet que recibe el cliente¶
<!-- BLIMX Help Center - widget loader -->
<script
src="https://widget.tudominio.com/static/v3/blimx-v3-chatbot-loader.js"
data-tenant="acme-helpcenter"
data-base-url="https://widget.tudominio.com"
data-frame="https://widget.tudominio.com/static/v3/dist/blimx-v3-chatbot-frame.html"
data-position="bottom-right"
data-color="#3B82F6"
defer></script>
El cliente solo cambia data-tenant (su bot.slug) y los colores. Todo lo demás se sirve desde el SaaS.
Atributos data-*¶
| Atributo | Obligatorio | Default | Descripción |
|---|---|---|---|
data-tenant |
✅ | — | Slug del bot. Identifica de qué Help Center cargar contenido |
data-base-url |
❌ | resuelto del src del script |
URL base del backend (donde está /v3/*) |
data-frame |
❌ | <base-url>/static/v3/dist/blimx-v3-chatbot-frame.html |
URL del HTML del iframe |
data-position |
❌ | bottom-right |
bottom-right, bottom-left, top-right, top-left |
data-color |
❌ | leído del frontend_config |
Color del launcher (override del que está en BD) |
data-launcher-icon |
❌ | icono question | URL de imagen custom |
data-z-index |
❌ | 2147483000 | Z-index del launcher e iframe |
Override desde el host page¶
El cliente puede setear globals antes de cargar el script:
<script>
window.blimxLang = 'fr'; // fuerza idioma
window.blimxChatbotDebug = true; // logs en consola
window.blimxPreferredLang = 'es'; // fallback si no se detecta
</script>
<script src="..." data-tenant="..."></script>
Arquitectura del widget¶
┌─────────────────────────────────────────────────────────┐
│ Página del cliente (cualquier dominio) │
│ │
│ <script src=".../blimx-v3-chatbot-loader.js" ...> │
│ │ │
│ │ inyecta dos elementos: │
│ ▼ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Launcher │ │ iframe (oculto │ │
│ │ (botón │ click │ hasta abrirse) │ │
│ │ flotante) │ ──────► │ │ │
│ │ │ │ src = data-frame │ │
│ └──────────────┘ │ ? lang=es │ │
│ │ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Modal Help Center│ │ │
│ │ │ (renderiza el │ │ │
│ │ │ bundle JS/CSS) │ │ │
│ │ └─────────────────┘ │ │
│ └──────────────────────┘ │
│ │ │
└───────────────────────────────────────┼────────────────┘
│ fetch
▼
┌─────────────────────────────────┐
│ widget.tudominio.com │
│ GET /v3/frontend/config │
│ GET /v3/faq?language=es │
└─────────────────────────────────┘
Por qué iframe (y no embed directo en DOM del cliente)¶
- Aislamiento CSS: los estilos del cliente no rompen el modal y viceversa.
- Aislamiento de eventos: el widget no captura submits/clicks de la web del cliente.
- Versionado: podemos desplegar versión nueva sin romper a clientes existentes (el iframe carga siempre el último bundle del SaaS).
Bundle del widget¶
Se compila con Vite + Terser:
Output:
static/v3/dist/
├── blimx-chatbot.min.js ~24 KB gzip
├── blimx-chatbot.min.css ~8 KB gzip
└── blimx-v3-chatbot-frame.html ~1 KB
El frame HTML referencia los bundles con paths relativos (./blimx-chatbot.min.css, ./blimx-chatbot.min.js) para que funcione con cualquier base-url que el SaaS sirva.
Componentes del widget (source)¶
static/v3/js2/
├── chatbot.js ← Entry; inicializa managers + FaqView
├── core/
│ ├── ApiService.js ← fetch a /v3/frontend/config y /v3/faq
│ ├── StateManager.js ← Pub/sub global
│ ├── UIManager.js ← Cambios de vista (faq, settings)
│ ├── LanguageManager.js ← Idioma actual + recarga FAQs en cambio
│ └── Telemetry.js ← (opt-in) eventos de uso
└── components/
├── Launcher.js ← Botón flotante (color, posición, ripple)
├── Modal.js ← Contenedor del modal
├── HeaderView.js ← Header con logo y selector idioma
├── NavbarView.js ← Tabs (en demo solo "FAQ")
└── FaqView.js ← Lista de FAQs con search + categorías
Otros componentes (HomeView, ChatView, ShopView, LeadCaptureView, ProcessView, etc.) fueron eliminados durante la transformación al "Help Center Builder". Si los necesitas, búscalos en el branch git de antes de mayo 2026.
Contrato de los endpoints públicos¶
GET /v3/health¶
Sin parámetros. Sin auth.
GET /v3/frontend/config¶
Auth de tenant: X-Client-ID: <bot_slug> o ?tenant=<bot_slug>.
{
"ui_config": {
"apiUrl": "https://widget.tudominio.com/v3",
"primaryColor": "#3B82F6",
"secondaryColor": "#1E40AF",
"companyLogo": "https://widget.tudominio.com/media/uploads/logos/acme.png",
"launcherPosition": "bottom-right",
"launcherIcon": null
},
"content": {
"default_language": "es",
"supported_languages": ["es", "en"],
"brand": {
"name": "ACME Help",
"showPoweredBy": true
},
"navItems": [
{ "id": "faq", "labels": { "es": "Ayuda", "en": "Help" } }
],
"texts": {
"noResults": { "es": "Sin resultados", "en": "No results" }
}
}
}
Errores:
- 400 si falta X-Client-ID o ?tenant=.
- 404 si el tenant no existe.
GET /v3/faq¶
Query params:
- tenant (o X-Client-ID header) — obligatorio.
- language — es | en | fr | de | pt. Default es.
- category — opcional, substring filter.
Auth: ninguna.
{
"tenant": "acme-helpcenter",
"language": "es",
"categories": [
{
"id": 12,
"name": "Precios",
"questions": [
{ "id": 101, "question": "¿Hay versión gratis?", "answer": "Sí, ..." },
{ "id": 102, "question": "¿Cómo pago?", "answer": "Aceptamos ..." }
]
},
{
"id": 13,
"name": "Soporte",
"questions": [...]
}
],
"total_questions": 47
}
Si el tenant no existe → 200 con categories: [] y total_questions: 0 (no 404, para no exponer enumeración de slugs válidos).
Detección de idioma (loader)¶
Tres signals dinámicos que actualizan el iframe sin recargar el script:
- MutationObserver sobre
<html lang>— cubre React Helmet, Vue i18n, Angular. - Intercept de
history.pushState/replaceState/popstate— cubre SPA routers. - Polling cada 2s — fallback para Wix, Shopify themes, WordPress AJAX.
Cuando detecta cambio, el iframe se recrea con el nuevo ?lang=XX en el src.
CORS y headers¶
El backend Django añade:
El widget puede embeberse en cualquier dominio. Si quieres restringirlo, añade en settings.py:
Flujo de carga (timing)¶
T+0 <script defer> se ejecuta tras DOMContentLoaded
T+10ms detectPageLang() → 'es'
T+15ms inyecta launcher (botón flotante visible)
T+20ms inyecta iframe oculto con src + ?lang=es
Usuario hace click en launcher
T+1s iframe se hace visible
T+1s bundle JS dentro del iframe se ejecuta:
├── fetch /v3/frontend/config?tenant=acme (~80ms)
└── fetch /v3/faq?tenant=acme&language=es (~120ms)
T+1.2s modal renderizado con FAQs
Métricas de un bot con 200 FAQs: bundle 24KB + config+faqs ~10KB → <35KB total.
Custom domain por cliente (opcional)¶
Si quieres que el widget cargue desde el dominio del cliente final (https://acme.com/help-center.js):
- Cliente añade un CNAME
help-cdn.acme.com → widget.tudominio.com. - Cliente embebe
<script src="https://help-cdn.acme.com/static/v3/blimx-v3-chatbot-loader.js" data-tenant="acme">. - Backend ya soporta este patrón (CORS abierto + tenant resolution por slug).
No requiere cambios de código del SaaS.
Build & deploy del widget¶
cd static/v3
npm install # primera vez
node build.js # regenera dist/
# El SaaS ya sirve /static/v3/* desde nginx. Tras un build, el iframe HTML carga el bundle nuevo automáticamente.
Para forzar invalidación de caché si has cambiado el contrato de la API:
- Cambia el version query string en el snippet:
data-version="2"(no implementado por defecto, pero el loader lo respetará).
Monitoring del widget¶
Endpoints útiles:
GET /v3/health— debe responder 200 < 50ms.GET /static/v3/blimx-v3-chatbot-loader.js— debe responder 200 conCache-Control: public, max-age=3600.
Si el widget no carga en producción, la lista de checks está en 17-troubleshooting.md.