Official Web Client for Cumments — a Matrix-backed decentralized comment system.
Status: Preview — usable for dogfooding, public API not yet stable. The browser client is functional, but the
<cumments-comments>attributes/events are still preview and may change before1.0.Note on demo backends:
https://comments.curious.hostis a personal test deployment, not a stable public service. It may be unavailable, reset, or change without notice. For anything beyond casual testing, deploy your own Cumments backend.
cumments-web is an embeddable, framework-independent Web Component for adding identity-aware, realtime discussions to static sites.
- Single custom element
<cumments-comments>— works with Zola, Hugo, Astro, or any static HTML. - No framework dependency for consumers — Lit 3 is an internal implementation detail.
- Identity-aware — per-browser Ed25519 identity stored locally, display name and avatar managed transiently.
- Realtime — SSE notifications with
GET-authoritative reconciliation, pagination, reactions, and polls. - Content-first — the comment feed is the primary surface; identity and composer tools are progressively disclosed.
Install the published browser artifact via CDN or build locally.
CDN (recommended for static sites):
<!-- Pinned, reproducible -->
<script type="module" src="https://cumments-web.curious.host/0.5.5/cumments-web.js"></script>
<!-- Preview channel (mutable, tracks latest tag) -->
<script type="module" src="https://cumments-web.curious.host/latest/cumments-web.js"></script>Local build:
pnpm install
pnpm build # emits dist/cumments-web.js + assets/pow.worker-*.jsSee CDN Distribution for versioning and worker co-location guarantees.
<cumments-comments
endpoint="https://your-cumments-instance.example.com"
site-id="my-blog"
page-slug="hello-world"
lang="zh-Hans"
></cumments-comments>Note:
https://comments.curious.hostin examples below refers to a personal test deployment. It is not a stable public service — deploy your own backend for production use.
Attributes:
| Attribute | Required | Description |
|---|---|---|
endpoint |
yes | API root, e.g. https://your-cumments-instance.example.com |
site-id |
yes | Site identifier |
page-slug |
yes | Page slug (PageSlug, not an arbitrary URL) |
per-page |
no | 1..100, default 20 |
lang |
no | BCP 47 tag, default en (zh-Hans / en shipped; cmn-Hans → zh-Hans) |
Properties mirror attributes (endpoint, siteId, pageSlug, perPage, lang). The component auto-creates an identity on first load, fetches the page, and connects realtime. Call element.reload() to re-fetch the current page. See Web Component for the full preview contract and locale resolution.
<cumments-comments>
|
v
AppRuntime
|
+-- IdentityFeature
+-- ProfileFeature
+-- CommentsFeature
+-- EditorFeature
+-- RealtimeFeature
- One public CustomElement —
<cumments-comments>is the only exported element;<cumments-editor>is internal (light DOM, no shadow). - Per-instance isolation — each
<cumments-comments>creates its ownAppRuntimeviaRuntimeController(the soleReactiveController). Multiple widgets on the same page do not share state. - Transport / signing boundaries —
HttpTransportis the solefetchowner;SigningPipelineis the sole canonicalization and signing boundary (challenge → PoW → sign).*Clienthelpers only build message parts. - No global singleton — no global store, event bus, DI container, or overlay manager. Transient UI (
openKey) and pending attachments (pendingMedia/pendingSticker/pendingLocation) are per-instance. - State primitives —
EntityCache(byId),PageView(order/meta),PendingOperation(pending submission) underpinCommentsFeature.
See Architecture for feature ownership and the request pipeline.
- Content-first feed — comment count, live status, and the comment list are the persistent layout. The composer stays collapsed until needed.
- Identity capsule and profile — a compact capsule shows the current profile (avatar and display name). Clicking it opens a popover (
role="dialog", non-modal) with a profile summary and actions. Profile (display name and avatar) is distinct from Identity (create, import, backup, mnemonic, switch, remove). Profile is managed via a dedicated dialog; cryptographic operations remain in identity dialogs. - Collapsed composer —
isCollapsedwhen unfocused and empty (!focused && !draft && !replyToId && !pending* && !uploading). Focus, draft content, reply, or any pending attachment expands it. - Composer profile context — the composer shows a read-only “Commenting as [avatar] Display Name” button (not an editable text field). Clicking it opens the profile dialog. The composer no longer exposes a per-comment display-name input; normal comments use the current profile display name.
- Progressive disclosure — secondary tools (Attach, Location, Sticker, Poll, Post) are subordinate to the main input. At
<480pxthe tools row wraps and uses icon-only labels without losing accessible names; the input remains the dominant control. - Reply workflow —
Reply(primary action) setsreplyToIdon the editor; the composer shows reply context with a cancel control. Submitting includesreplyToIdand thread-root derivation. - Reaction picker — persistent reaction summary with a
+trigger opens a transient picker (role="dialog", non-modal). Single-transient coordination viaopenKey; Escape and focus return are handled. - Poll rendering and voting — poll messages render with question, options, vote counts and total votes, single-choice radio selection, and a Vote button. The selected option is kept,
VOTEis sent viaPollsClient.vote(throughCommentsFeature.votePoll), loading disables controls, errors are shown per-poll with retry, and successful votes refresh via the existingGET-authoritative refresh (no SSE/live). Voting does not use/comments. - Sticker / media / location / poll — selecting a sticker, uploading media, sharing location, or creating a poll creates a pending state (
pendingSticker/pendingMedia/pendingLocation/pollDraftwith question and options) with a compact preview and remove control. Nothing submits until the user explicitly presses Post comment. All pending states are explicit composer attachments and are included incumments:submittogether with the current profile display name. Polls require a question (1–500 grapheme clusters) and 2–20 options (1–200 grapheme clusters each) and are validated inline before submission; polls are mutually exclusive with media, sticker, and location. - Comment actions — each comment exposes
Replyand⋯(menu). The⋯menu containsEdit,Copy link, andDelete(owner-only).Deleteuses a modal confirmation dialog with focus trapping;Copy linkwrites the comment URL vianavigator.clipboard. - Dialogs and pickers — transient surfaces stay inside the component
ShadowRoot, use appropriatedialog/menusemantics, supportEscape, and restore focus to the trigger. No portal outside the shadow tree. - Avatar — avatar upload and removal are backed by
PUT/DELETE /api/v1/sites/{site_id}/visitors/avatar. Display name updates are applied locally to the profile projection and persisted on the next comment via the existingdisplay_namefield; there is no dedicated display-name update endpoint.
cumments (backend) ── HTTP API v1 ──► cumments-web ── integration ──► Zola / Hugo / Astro / custom sites
cummentsowns domain, HTTP API, Matrix integration, persistence, projection, auth.cumments-webowns API client, browser identity, request signing, Proof-of-Work, comments/replies/reactions/polls, pagination, SSE/realtime, media/avatar, client state, UI and the final Web Component.- Static-site generators are consumers, not dependencies. Do not put Zola integration into
cumments.
Internal research and the phased implementation plan live in misc/design/ (git-ignored, local only). docs/ holds user-facing documentation.
cumments-web depends on the Cumments API contract, not a specific server git tag:
cumments-web 0.xsupports Cumments API v1.- Cumments API v1 remains the API version used throughout the
0.xseries, but it is not considered stable before both Cumments and cumments-web reach 1.0. - Once both Cumments and cumments-web reach 1.0, Cumments API v1 will receive compatibility guarantees. Breaking changes to v1 will require a new API version.
Compatible extensions (such as new optional request/response fields or new endpoints) do not break v1 clients. A breaking change is defined as: an old client operating according to the documented contract no longer works against the new server.
The OpenAPI contract lives in the backend repository at docs/public/openapi.yaml (OpenAPI 3.2.0) and is the canonical source for request and response types.
Browser artifacts are versioned and distributed via HTTPS CDN (GitHub Pages):
- Immutable version:
https://cumments-web.curious.host/0.5.5/cumments-web.js - Preview channel:
https://cumments-web.curious.host/latest/cumments-web.js(mutable, tracks latest tag)
<!-- Pinned, reproducible -->
<script type="module" src="https://cumments-web.curious.host/0.5.5/cumments-web.js"></script>
<!-- Preview / dogfooding (may contain breaking changes) -->
<script type="module" src="https://cumments-web.curious.host/latest/cumments-web.js"></script>
<cumments-comments endpoint="https://your-cumments-instance.example.com" site-id="my-blog" page-slug="hello-world" lang="zh-Hans"></cumments-comments>0.x allows breaking changes; latest is a moving alias. For reproducible deploys, pin a version. See CDN Distribution.
API correctness → Identity correctness → Security correctness → Realtime correctness → State model → Usable UI → Beautiful UI → Theming/integrations
Bundle size is monitored as a performance signal via pnpm build (reports raw and gzip). The project does not impose a strict 50 KB gzip ceiling; functional improvements may increase the bundle when the resulting size remains reasonable. Current production build is ~263 KB raw / ~67 KB gzip.
Prerequisites: devenv (provides Node.js 24 + pnpm + nixd). Backend checkout is expected at ../cumments for local contract reference (docs/public/openapi.yaml).
# enter dev shell (direnv does this automatically)
devenv shell
# install dependencies
pnpm install
# quality gates (all green on main)
pnpm lint # biome check .
pnpm typecheck # tsc --noEmit
pnpm test # vitest run (61 files / 880 tests)
pnpm build # vite build (ESM, gzip ~67k, reported at build time; no strict 50 KB ceiling — see bundle-size note below)MIT — same as cumments.