# [1.5.0](http://forgejo:3000/ircforever/website/compare/v1.4.2...v1.5.0) (2026-08-09) ### Features * **network:** list both bouncer ports with their TLS status ([ |
||
|---|---|---|
| .forgejo/workflows | ||
| deploy | ||
| legacy | ||
| src | ||
| .gitignore | ||
| .releaserc.json | ||
| CHANGELOG.md | ||
| DEPLOY.md | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| svelte.config.js | ||
| vite.config.js | ||
IRCForever — website
The IRCForever network site. SvelteKit + Tailwind v4, fully prerendered to
static HTML (adapter-static), with a BLoC state layer. Works everywhere:
JS browsers hydrate into a snappy SPA; Lynx / curl / crawlers get complete,
readable content with no JavaScript.
| asset | gzip |
|---|---|
| client JS (whole app) | ~49 kB |
| CSS | ~5 kB |
| each prerendered page | 13–18 kB HTML (full content baked in) |
Develop
pnpm install
pnpm dev # http://localhost:5173
pnpm build # → build/ (one static .html per route, deploy anywhere)
pnpm preview # serve the production build
Every route is prerendered to a real file (build/index.html, build/bnc.html,
build/network.html, build/shells.html), so it drops onto any static host with
no server or rewrite rules. Test the no-JS view: lynx build via pnpm preview,
or curl http://localhost:4173/network.
Changing the theme / colors
Everything visual flows from one place: the @theme block at the top of
src/app.css. Edit the tokens and the whole site re-skins —
Tailwind generates matching utilities (bg-surface, text-accent, …) and the
design-system component classes read the same custom properties.
@theme {
--color-bg: oklch(15% 0.014 252); /* page background */
--color-accent: oklch(83% 0.16 162); /* the mint accent */
--color-fg: oklch(96% 0.004 252); /* body text */
/* …surfaces, borders, status colors, fonts, radii… */
}
Want a different accent? Change --color-accent, --color-accent-dim,
--color-accent-ink and you're done. Colors are in oklch so lightness/chroma/hue
are independently tunable. To go back to a centered max-width column instead of
full-bleed, set --container-shell to e.g. 1180px.
Architecture
src/
app.html page shell (web fonts, favicon, %sveltekit% slots)
app.css Tailwind import + @theme (the theme) + component primitives
routes/ SvelteKit file-based routes (real URLs)
+layout.js export const prerender = true ← static prerender
+layout.svelte app shell: Nav + page-change animation + Footer
+page.svelte / → renders pages/Home
bnc/+page.svelte /bnc → renders pages/Bnc
shells/+page.svelte /shells → renders pages/Shells
network/+page.svelte/network → renders pages/Network
lib/
pages/ the page content components (Home/Bnc/Shells/Network)
components/ reusable UI: Nav, Footer, Terminal, ChatClient
blocs/ business logic, fully decoupled from UI ── the BLoC layer
router.bloc.js href() helper that builds real URLs (/bnc, /network#connect)
nav.bloc.js mobile menu open/close (Nav wires close() to afterNavigate)
tabs.bloc.js reusable tab-group factory
clipboard.bloc.js copy-to-clipboard with transient "copied" feedback
irc.bloc.js real IRC-over-WebSocket client for #IRCForever (see below)
legacy/ the original hand-written static HTML/CSS, kept for reference
The BLoC pattern here
Each bloc is a self-contained unit of business logic that exposes state out (Svelte stores) and events in (plain methods that mutate that state). UI components never hold business logic — they subscribe to a bloc's store(s) and call its methods.
Singletons (nav, clipboard) are exported as instances; tabs and irc are
factories so each instance is isolated. router.bloc.js is now just a
URL-builder — routing itself is SvelteKit's job.
In a component, destructure a bloc's store before using
$:const { open } = nav→ then$open.$nav.openwould (wrongly) try to subscribe to the bloc object itself.
Routing
File-based, real URLs (/, /bnc, /shells, /network). In-page sections are
native anchors (/network#connect, #rules, …) — SvelteKit scrolls to them, and
they work in Lynx with no JS. Nav highlights the active route from the $page
store and closes the mobile menu via afterNavigate. The fade-and-rise
page-change animation lives in +layout.svelte, keyed on the pathname and
honoring prefers-reduced-motion (intro transitions don't run on first hydrate, so
the prerendered page just appears).
Hero live chat (ChatClient.svelte + irc.bloc.js)
The hero is a real, interactive IRC client for #IRCForever. On mount it opens
wss://chat.ircforever.org/webirc/websocket/ (the network's KiwiIRC webircgateway,
relayd-proxied), registers with a random friendly nick (adjective+noun, e.g.
MellowOtter; retries on nick-in-use), and joins the lobby. IrcBloc parses the
protocol into structured messages: while connecting it surfaces the real sign-on
chatter (ident/hostname lookups, LUSERS, MOTD, topic) like any client; once you're
in the channel it goes quiet and shows just chat (joins/leaves as muted system
lines). The composer is styled as a shell prompt (nick@ircforever $) and only
sends plain messages to #IRCForever — no commands, no channel switching. The
title bar shows the channel and a live "N online" count.
Resilience & SSR: messages is prefilled with a short demo conversation, so the
prerendered / no-JS view is never empty (good for SEO). On the client start()
clears it and connects live; if the gateway can't be reached it falls back to that
demo, clearly labeled. Browsers can't open raw TCP and ngircd has no WebSocket — the
gateway is what makes a browser client possible.
Deploy note: the page's own CSP must allow
connect-src wss://chat.ircforever.orgfor the live client to connect.