# Frontend and components The client side is vanilla JavaScript that enhances server-rendered HTML in place. This page covers the no-framework rule, the `Application` root, the custom web components, the content rendering pipeline, and the declarative `data-*` contract. See also [Styling and templates](/docs/architecture-styling.html). > Audience: administrators and maintainers. These pages are hidden from members and guests, in the sidebar, the search index, and the documentation export. ## Vanilla JavaScript, no framework All JavaScript lives in `static/js/` as **ES6 modules, one class per file**. There is no React, Vue, or Angular, no NPM, and no bundler: the browser loads native modules directly. A small set of libraries is loaded from `static/vendor/` over CDN-style script tags: `marked` (markdown), `highlight.js` (syntax highlighting), `DOMPurify` (sanitization), and `emoji-picker-element`. Those vendor scripts MUST be loaded with `defer` or `type="module"`. The Playwright tests wait on `domcontentloaded`; a blocking script would make that event time out. ## The Application root `Application.js` is the browser-side root. A single instance is created and exposed globally: ```javascript const app = new Application(); window.app = app; ``` Its constructor imports `components/index.js` (which defines every web component) and instantiates every page controller as a property (`app.modals`, `app.forms`, `app.votes`, `app.comments`, and so on). It also creates the three component singletons and exposes them as `app.dialog`, `app.contextMenu`, and `app.toast`. Page code reaches any capability through `window.app`. ## Custom web components Reusable UI widgets are custom elements with the `dp-` prefix, living in `static/js/components/`. Each extends a thin `Component` base that adds typed attribute helpers: ```javascript export class Component extends HTMLElement { attr(name, fallback = "") { ... } boolAttr(name) { ... } intAttr(name, fallback = 0) { ... } } ``` Every component **renders into the light DOM (no shadow root)** so the global CSS applies, and **self-registers** with `customElements.define` at the bottom of its file. `components/index.js` imports them all and is imported by `Application.js`, so every element is defined site-wide. The catalog: | Element | Purpose | |---------|---------| | `dp-avatar` | Renders a user avatar. | | `dp-code` | Renders a syntax-highlighted code block. | | `dp-content` | Client-side content renderer for live content; server content uses the backend `render_content` Jinja global. | | `dp-title` | Inline title-sized companion to `dp-content` for live titles. | | `dp-upload` | The single file-upload button used everywhere. | | `dp-lightbox` | The single full-size image lightbox (`app.lightbox`), attribute-wired via one delegated listener. | | `dp-toast` | Transient notifications (`app.toast`). | | `dp-dialog` | Confirm / prompt / alert modals (`app.dialog`). | | `dp-context-menu` | Right-click and long-press menus (`app.contextMenu`). | **The component-vs-module rule.** Self-contained presentational widgets become components. Partial-bound controllers that enhance server-rendered markup (votes, reactions, comments) and pure utilities stay plain modules, because they augment existing HTML rather than render standalone UI. **Content and titles render on the backend.** For SEO, all content and titles that exist at request time are rendered server-side by the `render_content` and `render_title` Jinja globals (`devplacepy/rendering.py`, built on mistune with full feature parity to the client renderer: emoji shortcodes, markdown, sanitisation, and YouTube/video/audio/image embeds, mentions, and autolinks). The `dp-content`/`dp-title` client components and the `data-render` attribute are now reserved for genuinely live content that is generated in the browser after load (chat answers, newly posted comments, live direct messages, the planning report). Server-rendered code blocks are still syntax-highlighted client-side, and content images are wired to the lightbox via a delegated listener. ## Shared utilities A few plain modules own the cross-cutting patterns. Reach for these instead of hand-rolling a fetch, an interval, or a click handler: - **`Http`** (`static/js/Http.js`) is the single fetch helper: `getJson`, `sendForm` (form-encoded POST that follows the login redirect), and `send` (POST that throws the server's `error.message` on a non-2xx response or a `200 {ok: false}` body). The container, admin, and project-files UIs all go through it rather than calling `fetch` directly. - **`Poller`** (`static/js/Poller.js`) wraps the universal live-update loop: `new Poller(fn, intervalMs, { pauseHidden })` with `start`/`stop`/`tick`, swallowing per-tick errors so one failed poll never kills the loop. The live-update controllers (notification counts, the service monitor, AI usage, the container manager and instance pages, the bot fleet) now take their fast updates from the pub/sub bus and keep a `Poller` only as a slow initial-load and fallback loop. See [Pub/Sub service](/docs/services-pubsub.html) for the live view relay that drives them. - **`PubSubClient`** (`static/js/PubSubClient.js`, `app.pubsub`) is the single websocket client for the pub/sub bus: `subscribe(topic, cb)` returns an unsubscribe function and `publish(topic, data)` sends a frame. It connects lazily, reconnects with backoff (with a fast retry on the lock-owner bounce), and re-subscribes after a reconnect. The live notification toasts, badge counts, and the admin live views all subscribe through it instead of polling. - **`JobPoller`** (`static/js/JobPoller.js`) is the frontend half of the async job framework: `JobPoller.run(statusUrl, { onDone, onFailed, onTimeout })` polls the status URL and fires one callback. The zip download and project fork flows share it. - **`OptimisticAction`** (`static/js/OptimisticAction.js`) is the base for click-to-POST engagement controllers. `VoteManager`, `ReactionBar`, `BookmarkManager`, and `PollManager` extend it and call `submit(url, params, errorTarget, render)` for the shared POST -> render -> error path, keeping only their own event wiring and render. - **`FloatingWindow`** (`static/js/components/FloatingWindow.js`) is the draggable, resizable window base shared by the container terminals and the Devii terminal; `WindowManager` (`app.windows`) gives every window a shared last-clicked-on-top z-order. ## Content rendering pipeline `ContentRenderer.js` runs on every element marked `data-render`, in this exact order: 1. emoji shortcode -> unicode 2. markdown via `marked` 3. **`DOMPurify.sanitize`** 4. `highlight.js` on `

    `
    5. image URL -> ``
    6. YouTube URL -> trusted iframe
    7. autolink remaining URLs

    A `NodeIterator` skips `CODE`, `PRE`, `SCRIPT`, and `STYLE` so URLs inside code are never rewritten. Sanitization runs on the raw `marked` output before the trusted YouTube embeds are injected, so user content cannot inject script, event-handler, or iframe payloads while our own embeds survive.

    This step is **fail-closed**: `render()` throws if `DOMPurify` is not loaded rather than emitting unsanitized HTML. Because content is rendered client-side from `element.textContent`, Jinja autoescaping does not cover it, so this sanitize step is the XSS control.

    ## The data-* contract

    Controllers find and enhance markup through declarative `data-*` attributes rather than inline scripts, keeping behavior HTML-driven and auditable (grep for the attribute):

    | Attribute | Behavior |
    |-----------|----------|
    | `data-render` | Render markdown content through the pipeline. |
    | `data-modal` | Open the named `.modal-overlay`. |
    | `data-confirm` | Show a confirm dialog before the action. |
    | `data-copy` | Copy the referenced content to the clipboard. |
    | `data-share` | Copy a share URL. |
    | `data-mention` | Attach `@mention` autocomplete to a textarea. |