Saltar a contenido

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:

cd static/v3
npm install
node build.js

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.

{
  "status": "healthy",
  "service": "widget",
  "timestamp": "2026-05-24T07:00:00+00:00"
}

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

  1. MutationObserver sobre <html lang> — cubre React Helmet, Vue i18n, Angular.
  2. Intercept de history.pushState/replaceState/popstate — cubre SPA routers.
  3. 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:

Access-Control-Allow-Origin: *
Cache-Control: no-store

El widget puede embeberse en cualquier dominio. Si quieres restringirlo, añade en settings.py:

CORS_ALLOWED_ORIGIN_REGEXES = [
    r'^https://([a-z0-9-]+\.)?clientepermitido\.com$',
]

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

  1. Cliente añade un CNAME help-cdn.acme.com → widget.tudominio.com.
  2. Cliente embebe <script src="https://help-cdn.acme.com/static/v3/blimx-v3-chatbot-loader.js" data-tenant="acme">.
  3. 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 con Cache-Control: public, max-age=3600.

Si el widget no carga en producción, la lista de checks está en 17-troubleshooting.md.