feat(website): add the project site and deploy it to GitHub Pages

A Vite + React + TypeScript + Tailwind v4 site under website/, built from
the soulseek-rs design system so the site and the TUI stay one product.
The colour tokens in src/index.css are the same values as
soulseek-rs/src/ui/styles.rs; change one, change both.

The hero is a working recreation of the TUI: it plays one scripted run and
then takes the keyboard, or lands on the resting state directly under
prefers-reduced-motion. Its sample data is invented or public domain, per
the tone-of-voice guardrail against naming copyrighted material.

Static output rather than an SPA shell. `bun run build` compiles the client,
compiles the same app for Node, then renders every route in src/lib/routes.ts
to its own file with that route's title, description and canonical URL. A
direct link to /install therefore answers 200 with real markup instead of
GitHub's 404 page. routes.ts is keyed into App.tsx by literal path, so adding
one without the other fails typecheck rather than shipping a page whose body
says "not found" under the right title.

Theme is resolved once by an inline script before first paint and adopted by
React through useSyncExternalStore, so there is no flash and no hydration
mismatch on the toggle. Light mode re-cuts the semantic hues darker: phosphor
and tape read on vinyl but wash out on manila.

CI and release skip website-only pushes, and Pages skips Rust-only ones. The
two Rust workflows share one paths-ignore list so they can only skip
together, which keeps the release ci-gate from polling for a run that was
never queued. That gate also now treats a cancelled CI as "keep waiting"
rather than a failure, since cancel-in-progress cancels the older SHA on
every back-to-back push.
This commit is contained in:
Michel de Graaf
2026-07-24 23:42:41 +02:00
parent 54feb3bc5a
commit 33bfc34dc8
58 changed files with 5239 additions and 1 deletions
+22
View File
@@ -0,0 +1,22 @@
/** Every off-site destination, in one place. */
export const LINKS = {
gh: 'https://github.com/michel/soulseek-rs',
issues: 'https://github.com/michel/soulseek-rs/issues',
releases: 'https://github.com/michel/soulseek-rs/releases',
changelog: 'https://github.com/michel/soulseek-rs/blob/master/CHANGELOG.md',
license: 'https://github.com/michel/soulseek-rs/blob/master/LICENSE',
cratesClient: 'https://crates.io/crates/soulseek-rs',
cratesLib: 'https://crates.io/crates/soulseek-rs-lib',
docsrs: 'https://docs.rs/soulseek-rs-lib',
rustup: 'https://rustup.rs',
soulfind: 'https://github.com/soulfind-dev/soulfind',
nicotine: 'https://nicotine-plus.org',
slskd: 'https://github.com/slskd/slskd',
slsknet: 'https://www.slsknet.org',
} as const
/** Injected from the workspace Cargo.toml at build time — see vite.config.ts. */
export const VERSION = __APP_VERSION__
/** Where the site is served from. Used for canonical and og:url. */
export const SITE_URL = 'https://michel.github.io/soulseek-rs/'
+61
View File
@@ -0,0 +1,61 @@
export interface RouteMeta {
/** Path without the base prefix, always leading-slashed. */
path: string
title: string
description: string
}
/**
* The single source of truth for what pages exist.
*
* Read three times: React Router matches against it, the layout applies the
* meta on navigation, and scripts/prerender.ts walks it to decide which HTML
* files to write. Adding a page means adding one entry here plus its route.
*/
/*
* `as const satisfies` rather than a plain annotation: the annotation widened
* `path` to `string`, which is what let App.tsx's hand-written route list
* drift from this one without failing the build. Literal paths make the
* mapping in App.tsx exhaustive-checkable.
*/
export const ROUTES = [
{
path: '/',
title: 'soulseek-rs, a Soulseek client for your terminal',
description:
'soulseek-rs is a keyboard-driven Soulseek client and a Rust protocol library. Search, share, browse, and chat from your terminal.',
},
{
path: '/install',
title: 'soulseek-rs, install',
description:
'Install the soulseek-rs client with cargo, build it from source, or depend on the protocol library and write your own client.',
},
{
path: '/docs',
title: 'soulseek-rs, docs',
description:
'Quick start for soulseek-rs: install, run, drive it from the keyboard, and use the shell subcommands.',
},
{
path: '/community',
title: 'soulseek-rs, community',
description:
'How to help with soulseek-rs, and where to go when another Soulseek client fits you better. MIT licensed. No telemetry.',
},
] as const satisfies readonly RouteMeta[]
/** Every path the site serves, as literals. */
export type RoutePath = (typeof ROUTES)[number]['path']
export const NOT_FOUND_META: RouteMeta = {
path: '/404',
title: 'soulseek-rs, not found',
description: 'That page does not exist.',
}
export const metaFor = (pathname: string): RouteMeta => {
const trimmed =
pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname
return ROUTES.find((route) => route.path === trimmed) ?? NOT_FOUND_META
}
+4
View File
@@ -0,0 +1,4 @@
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export const cn = (...inputs: ClassValue[]): string => twMerge(clsx(inputs))