Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
- [ ] Update the `[Unreleased]` compare link to the new tag.
- [ ] Create and push a signed `vX.Y.Z` tag from `master`.

### Changed

- Marketing site (<https://runner.kjanat.dev>) rewritten as a
SvelteKit 2 / Svelte 5 prerendered static app on Cloudflare. Seven
composable section components, build-time CHANGELOG.md ingestion
via a SvelteKit server-load + Vite `?raw` import, ESLint with the
Svelte plugin, and a prerender-output validation suite
(freshness-aware in CI). Added `[package.metadata.site].default-
branch` so the footer changelog link and installer-script URL
share one source.

## [0.11.0] - 2026-05-19

### Added
Expand Down
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ autobins = false
name = "Kaj Kowalski"
email = "info+runner@kajkowalski.nl"

[package.metadata.site]
# Single source of truth for the marketing site. The default-branch
# value is interpolated into both the footer "changelog" link and the
# `linuxInstaller` raw-githubusercontent URL — so a future branch
# rename is a one-line fix here, not a search-and-replace across the
# site tree. Parsed by site/scripts/site-data.ts.
default-branch = "master"

[package.metadata.npm]
name = "runner-run"
subpkgscope = "@runner-run"
Expand Down
474 changes: 437 additions & 37 deletions bun.lock

Large diffs are not rendered by default.

182 changes: 182 additions & 0 deletions docs/superpowers/specs/2026-05-19-site-sveltekit-rewrite-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Site rewrite: SvelteKit, composable, a11y-first

Date: 2026-05-19
Branch: `site/sveltekit-rewrite`
Status: design approved, pending spec review

## Problem

`site/` is a hand-rolled static site: a 211-line single-page
`src/index.html` with `{{var}}` placeholders, custom `build.ts` /
`dev.ts` Bun scripts doing the templating and bundling, plain CSS, and
a 23-line `copy.ts` for clipboard buttons. Deployed to Cloudflare
Workers via `wrangler` at `runner.kjanat.dev`.

Two problems: (1) the build pipeline is bespoke and hard to reason
about ("scripts and fuckery"); (2) the front page crams seven distinct
content blocks into one cluttered scroll.

## Goals

- Replace the bespoke build with a standard framework.
- Decompose the page into self-contained blocks that can be freely
recomposed into pages ("composable for easier toying around").
- Preserve the existing visual design language (no redesign); the
front page may change moderately, need not be pixel-identical.
- First-class accessibility.
- Keep the Cloudflare deploy target, domain, and edge headers.

## Non-goals (YAGNI)

No CMS, no i18n, no analytics rework, no docs system, no visual
redesign, no new content. Same content and look, recomposed.

## Stack

- SvelteKit 2 + Svelte 5 (runes) + TypeScript.
- `@sveltejs/adapter-cloudflare`.
- **Fully prerendered**: `export const prerender = true` in the root
layout. Output is static; no runtime server. Rationale: a marketing
site has no per-request state; prerender maximizes load speed and SEO
and keeps the Cloudflare deploy a static upload.
- Vite dev server replaces `dev.ts`; `vite build` replaces `build.ts`.
Both scripts are deleted.

## Architecture: composable blocks

The composability is the architecture. Three layers:

### 1. Content — single typed source

`src/lib/content/site.ts` exports one typed object:

```ts
export interface SiteData {
npmName: string;
cratesName: string;
repo: string; // https://github.com/kjanat/runner
version: string; // e.g. "0.11.0"
domain: string; // runner.kjanat.dev
}
```

`version` (and any other release-derived value) is resolved **at build
time** from the workspace root `Cargo.toml` and emitted into a
generated, git-ignored `src/lib/content/generated.ts` by a prebuild
step (`scripts/gen-site-data.ts`, run via the `prebuild` npm script and
in CI before `vite build`). `site.ts` imports the generated values and
re-exports the typed `SiteData`. This replaces `build.ts`'s `{{var}}`
string templating with a typed module — single source, never stale,
fails the build loudly if `Cargo.toml` can't be read or parsed.

### 2. Sections — one self-contained component per block

`src/lib/sections/`, one component per current block:

- `Wordmark.svelte` — wordmark + tagline + meta (the `<header>`).
- `Install.svelte` — install command buttons.
- `Demo.svelte` — the "what it looks like" terminal transcript.
- `Completion.svelte` — completion setup + transcript.
- `Speaks.svelte` — the "it speaks" PM/runner support matrix.
- `Why.svelte` — the "why it's not shit" rationale list.
- `SiteFooter.svelte` — the footer.

Each component: owns its markup and component-scoped `<style>`, takes
typed props (data it needs is passed in, not reached for), no knowledge
of which page it sits on. Acceptance per component: it can be dropped
into any page with its props and render correctly in isolation.

Shared interaction: the `copy.ts` clipboard logic becomes a reusable
`<CopyButton>` component plus a `use:copyable` Svelte action, consumed
by `Install` and `Completion`. It keeps the existing
`aria-live`/`role="status"` copied-feedback behavior (see a11y).

### 3. Routes — thin compositions

`src/routes/` pages are thin: a page is an ordered list of section
components. Default composition:

- `/` — `Wordmark`, `Install`, a condensed `Demo` teaser, nav links.
- `/demo` — `Demo`, `Speaks`.
- `/completion` — `Completion`.
- `/why` — `Why`.
- `SiteFooter` + nav in the root layout (present on every page).

Re-splitting the site later = editing the composition in one
`+page.svelte`; no section internals change. This is the explicit
payoff of the block design.

## Styling

- `src/lib/styles/tokens.css` — design tokens (color, type scale,
spacing, radii) extracted from the current CSS, imported once in the
root layout. The existing look is preserved; centralizing tokens
makes a future tweak a one-file change.
- Global resets/base from current `base.css` → root layout global
stylesheet. Section-specific rules move into the relevant
component's scoped `<style>`. `index.css`/`404.css` are ported, not
reinvented; the rendered design language is unchanged.

## Accessibility (first-class)

- Semantic landmarks: one `<main>` per page, `<nav>` for site nav,
`<header>`/`<footer>` in the layout; every section labelled
(`aria-labelledby` on its heading), preserving the current pattern.
- Skip-to-content link as the first focusable element in the layout.
- Copy buttons: real `<button>`, keyboard-operable, with the existing
`role="status"` `aria-live="polite"` region announcing
copied/failed. Visible focus styles (no focus suppression).
- Route changes: move focus to the page `<h1>`/`<main>` and announce
via a polite live region so keyboard/SR users aren't stranded
(SvelteKit `afterNavigate`).
- `prefers-reduced-motion`: the terminal cursor/typing animation and
any transitions are disabled under the media query.
- Color contrast: tokens chosen/verified to meet WCAG 2.1 AA
(≥ 4.5:1 body text, ≥ 3:1 large text/UI).
- The wordmark SVG has an accessible name; decorative SVGs are
`aria-hidden`.

## Testing (scaled to a marketing site — light but real)

- `svelte-check` + `tsc` clean (no `any`, no suppressions).
- `vite build` prerenders **every** route with zero warnings; CI fails
on prerender warnings or unresolved internal links.
- Unit: `src/lib/content/site.ts` exposes the canonical names; a test
asserts every install/completion command string in `Install`/
`Completion` is built from `site.ts` values (no hardcoded drift).
- Accessibility: automated `axe-core` scan (Playwright +
`@axe-core/playwright`) over every prerendered route, zero
violations; a keyboard-only smoke test that the skip link works and
copy buttons are reachable and announce.
- `scripts/gen-site-data.ts` has a test for the Cargo.toml→version
parse, including the failure path (missing/invalid manifest → build
error, not a silent empty version).

## Deploy

- `@sveltejs/adapter-cloudflare`; `wrangler.jsonc` updated to serve the
adapter output. Domain `runner.kjanat.dev` unchanged.
- `public/_headers` and `public/robots.txt` carried over (SvelteKit
`static/`).
- `site/package.json` scripts: `dev` → `vite dev`, `build` →
`prebuild` (gen-site-data) + `vite build`, `deploy` → build +
`wrangler deploy`, `check` → `svelte-check`, plus `test`. `build.ts`,
`dev.ts`, old `copy.ts`, `biome.json` if superseded by the SvelteKit
toolchain — removed once their function is replaced (not before).

## Risks / decisions

- Prerender vs SSR: prerender chosen; no dynamic data exists. If a
future need arises it is a one-line adapter/route change, not a
rearchitecture.
- Build-time `version`: depends on the site building from within the
repo (it does, in CI and locally). A detached build would fail
loudly — acceptable and preferable to a stale value.
- Toolchain churn (Biome vs SvelteKit's eslint/prettier story):
resolved during planning; not a design fork.

## Out-of-scope cleanup explicitly deferred

The stale `site/dist/` and `site/.wrangler/` artifacts in the working
tree are not part of this design; their handling (gitignore/removal)
is an implementation-plan task, noted so it is not lost.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"npm/facade"
],
"devDependencies": {
"@types/node": ">=22.18.0",
"typescript": "^5"
"@types/node": "^25.9.1",
"typescript": "^6.0.3"
},
"volta": {
"node": "24.14.1",
Expand Down
9 changes: 9 additions & 0 deletions site/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules/
/.svelte-kit/
/build/
/dist/
/.wrangler/
# Generated at build time from workspace-root Cargo.toml by
# scripts/gen-site-data.ts (prebuild). Never hand-edited, never committed.
/src/lib/content/generated.ts
.DS_Store
47 changes: 47 additions & 0 deletions site/DISCOVERIES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Discoveries — runner site

Hard-won, repo-specific findings. Read before "finishing" the CSS
scoping or touching the terminal sections.

## Svelte scoped styles never reach `{@html}` content

`Demo.svelte` and `Completion.svelte` render their terminal
transcripts as trusted static strings via `{@html}` (the
Svelte/dprint formatter reflows element markup and corrupts the
whitespace-exact ASCII otherwise — see those components' comments).

Svelte adds its scope hash only to elements in a component's
*template*. `{@html}` output is injected at runtime and gets **no
scope class**. Therefore every `.term`, `.term .dim/.bold/.green/
.link/.arrow/.cursor/.prompt` rule **must stay in global
`app.css`** (or be wrapped in `:global()`). Moving them into a
component `<style>` silently unstyles the terminal.

## Some display classes are shared — keep them global

`.section-tag` (5 sections), `.tagline` (Wordmark + Completion +
404), `.meta` (most sections + 404), `.install` (Install +
Completion), `hr.rule` (route pages), and `.wordmark` (Wordmark
**and `routes/+error.svelte`'s 404**) are authored in more than one
component. Svelte cannot share one component's scoped style with
another, so these are global by necessity. The `.wordmark`/404
coupling was caught only by a post-refactor selector-accounting
sweep — re-run that sweep after any further scoping (`-F` = fixed-string, so
selectors like `.copy .toast` aren't treated as regexes):
`for sel in ...; do grep -rlF "$sel" src/app.css src/lib src/routes; done`

## What is legitimately scoped

`.copy*` → CopyButton, `.matrix*` → Speaks, `.why*` → Why,
`footer*` → SiteFooter, `nav.site`/`.skip-link` → +layout. These
elements are authored in exactly one component's template.

## Roadmap (not deferred indefinitely — next, in order)

1. Extract the `:root` token block into `src/lib/styles/tokens.css`
imported by the layout (pure separation, zero render change;
the spec's original intent). Low risk, do soon.
2. Automated a11y: `@axe-core` over every prerendered route.
3. Visual-regression guard (pixel diff) so CSS refactors stop
relying on manual review — the gap that let the wordmark/nav
overlap ship.
File renamed without changes.
63 changes: 0 additions & 63 deletions site/biome.json

This file was deleted.

Loading