No description
Find a file
forgejo-actions 63637be860 chore(release): 1.5.0 [skip ci]
# [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 ([b22a816](b22a816301))
2026-08-09 15:03:18 +00:00
.forgejo/workflows fix: run under ircforever.org CSP via nonce-stamped inline script 2026-06-20 14:38:38 -04:00
deploy fix: repoint current symlink with ln -sfh on deploy 2026-06-20 14:46:14 -04:00
legacy feat: initial release of the IRCForever website 2026-06-20 13:30:45 -04:00
src feat(network): list both bouncer ports with their TLS status 2026-08-09 11:01:53 -04:00
.gitignore feat: initial release of the IRCForever website 2026-06-20 13:30:45 -04:00
.releaserc.json ci: match org release setup (cycjimmy action, @saithodev/semantic-release-gitea, REGISTRY_TOKEN) 2026-06-20 13:46:33 -04:00
CHANGELOG.md chore(release): 1.5.0 [skip ci] 2026-08-09 15:03:18 +00:00
DEPLOY.md fix: run under ircforever.org CSP via nonce-stamped inline script 2026-06-20 14:38:38 -04:00
package.json ci: publish releases with the automatic token; drop commitlint job 2026-06-20 13:54:00 -04:00
pnpm-lock.yaml feat: initial release of the IRCForever website 2026-06-20 13:30:45 -04:00
pnpm-workspace.yaml feat: initial release of the IRCForever website 2026-06-20 13:30:45 -04:00
README.md feat: initial release of the IRCForever website 2026-06-20 13:30:45 -04:00
svelte.config.js feat: initial release of the IRCForever website 2026-06-20 13:30:45 -04:00
vite.config.js feat: initial release of the IRCForever website 2026-06-20 13:30:45 -04:00

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.open would (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.org for the live client to connect.