# Converse.js v14 blank page: websocket_url fix + auth verification
Follow-up to `webchat-conversejs-and-caddy-trap.md` (same ICQ project, 2026-08-28).
These lessons were confirmed after the initial web client deployment: the page served
HTML/CSS/JS fine but rendered NOTHING.
## ⚠️ ROOT CAUSE #1 (FIXED 2026-08-28): Converse v14 is an ES MODULE, not UMD
The v14 dist bundle (`converse.min.js`) is built as a genuine ESM module:
- it uses `import.meta.url` (webpack auto-publicPath) and ends with `export{c as default}`;
- it lazy-loads chunks via dynamic `import("./" + chunkName)` (e.g. `chunkjs/locales/...`).
Loading it with a classic `
```
Verification (headless chromium, no extra installs — chromium is in snap on bigbox):
```bash
chromium --headless --no-sandbox --disable-gpu --virtual-time-budget=15000 \
--dump-dom https://chat.nixg.ru/ | grep -c converse-login-form # 1 = UI rendered
# console errors BEFORE the fix:
chromium --headless --no-sandbox --enable-logging=stderr --virtual-time-budget=20000 \
--dump-dom https://chat.nixg.ru/ 2>&1 | grep -iE 'CONSOLE|Uncaught'
```
## ROOT CAUSE #2 (earlier, already fixed): `bosh_service_url` vs `websocket_url`
| Option | What Converse does |
|--------|-------------------|
| `websocket_url: 'wss://host/xmpp-websocket'` | Connects via WebSocket. **Use this.** |
| `bosh_service_url: 'wss://host/xmpp-websocket'` | Treats the URL as BOSH (HTTP long-polling endpoint). With a wss:// URL and no BOSH handler on the server, the connection fails SILENTLY and the UI never renders. |
Evidence from the v14 bundle:
```js
function d1() {
return ("WebSocket" in window || "MozWebSocket" in window)
&& cZ.get("websocket_url") ? cZ.get("websocket_url")
: cZ.get("bosh_service_url") ? cZ.get("bosh_service_url") : "";
}
```
So `websocket_url` takes priority; `bosh_service_url` is only a BOSH fallback. Passing a
wss:// URL via `bosh_service_url` is a guaranteed blank page.
## Locales are embedded in v14
The bundle contains all translations as lazy-loaded webpack chunks
(`./src/i18n/locales/*/LC_MESSAGES/converse.po`). `i18n: 'ru'` needs NO external locale
files — do not go hunting for .po files or a locales_path.
## `assets_path` default is `/dist`
Defaults contain `assets_path: "/dist"`. If you deploy bare files (converse.min.js/css in
the web root) rather than the full dist layout, set `assets_path` explicitly to a path
that exists, or Converse may look for extra assets under /dist and fail. This is the
leading hypothesis when the page is STILL blank after the websocket_url fix.
## Diagnosing a blank page without a browser
Check the nginx (webchat container) access log: **a healthy Converse loads JS/CSS with
200s AND then issues a GET to /xmpp-websocket** (the WS upgrade). If css/js get 200s but
NO /xmpp-websocket request ever appears, the client is failing during
initialize/rendering BEFORE it attempts the socket — i.e. a client-side config/asset
problem, not a server problem. Confirm the server side separately (see below) so you know
the failure is client-side.
```bash
docker logs icq-webchat --since 10m | grep -vE "GET /converse\.min" | tail -20
# look for: GET /xmpp-websocket (or absence of it)
```
## Verify auth end-to-end over WebSocket (slixmpp)
Proves the whole path (browser → Caddy → nginx → Prosody → SASL) works even when the web
client is broken. slixmpp needs a no-verify SSL context for self-signed certs:
```bash
pip install slixmpp --break-system-packages
```
```python
import asyncio, ssl, slixmpp
class Bot(slixmpp.ClientXMPP):
def __init__(self, jid, password):
super().__init__(jid, password)
self.add_event_handler('session_start', self.on_start)
self.add_event_handler('failed_auth', self.on_failed)
self.add_event_handler('disconnected', self.on_disconnect)
async def on_start(self, event):
print("AUTH OK:", self.boundjid)
await self.disconnect()
def on_failed(self, event):
print("AUTH FAIL"); self.disconnect()
def on_disconnect(self, event):
self.stop()
async def main():
bot = Bot('user@chat.nixg.ru', 'PASSWORD')
ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
bot.ssl_context = ctx
bot.connect(('wss://chat.nixg.ru/xmpp-websocket',)) # full external path incl. TLS
await asyncio.wait_for(bot.disconnected, timeout=25)
asyncio.run(main())
```
Expect `AUTH OK: user@chat.nixg.ru/...`. Notes:
- Direct 5222 TCP test will ALSO hit `SSLCertVerificationError` on self-signed certs —
the `ssl_context` override fixes both, but prefer the wss:// test (validates Caddy TLS
and the WS proxy too).
- `slixmpp.connect()` has NO `reattempt=` kwarg in current versions — omit it.
- `prosodyctl register user domain pass` inside the container is how you set/reset a
password (idempotent, works even if the account exists).
## CDN-hardening: local assets
cdn.conversejs.org may be unreachable/slow from RF. Download the release once and serve
locally:
```bash
cd /opt/icq/webchat
curl -sL -o converse.min.js https://cdn.conversejs.org/dist/converse.min.js
curl -sL -o converse.min.css https://cdn.conversejs.org/dist/converse.min.css
# NOTE: https://cdn.conversejs.org/css/converse.min.css does NOT exist (404); the css
# lives under /dist/ in v14. Check with `curl -sI` and `head -c 100` — a 1.4KB "css" is a 404.
```
Verify the downloaded JS parses: `node --check converse.min.js` → "JS СИНТАКСИС OK".
(If `node` isn't present, `which node` first; the Hermes sandbox runs it from ~/.local/bin.)