Skip to content

Commit 9efe08a

Browse files
authored
feat(website): kolu.dev — Astro landing page + editorial blog (#625)
## Summary Gives kolu a home on the web. A **landing page** in the project's own voice — *agent-agnostic, zero-setup, terminal-first* — and a **blog** whose first post is the xterm.js `IntersectionObserver` leak investigation at `/blog/xtermjs-perf`. Served from the custom domain **kolu.dev** via GitHub Pages; built reproducibly by the root Nix flake. ## The site *Editorial-terminal aesthetic.* Near-black background with a single warm amber accent drawn from the tiered-step favicon — the rainbow lives in the favicon, everywhere else is restraint. Self-hosted **Fraunces Variable** for display and body, **JetBrains Mono Variable** for code and eyebrow labels, installed via fontsource so the build is Nix-reproducible with no runtime CDN fetch. A subtle dot-grid background evokes character cells; the only motion on the whole site is a single blinking block cursor in the hero. **Light/dark toggle** lives in the header. An inline FOUC-safe bootstrap reads ` localStorage` (falling back to `prefers-color-scheme`) and sets `data-theme` on `<html>` before first paint. Shiki emits dual-theme CSS variables for code blocks, routed by `data-theme` so the code lightness tracks the toggle too. **Blog typography** uses `tailwindcss-typography`'s `.prose` plus a `.prose-editorial` layer that owns the color palette and optical-size overrides. A drop cap opens each post. Bold text is pure white in dark mode / near-black in light mode — kept structurally distinct from inline code (which uses amber-bright), so emphasis and code read as two separate visual channels. The first post embeds the bus-stop tweet inline via `platform.twitter.com/widgets.js`. ## Integration *Not a standalone flake.* `packages.\${system}.website` is exposed by the root `flake.nix`, which builds the site through a synthesized src — the working tree has `website/public/favicon.svg` as a symlink to `packages/client/favicon.svg`, and the root flake resolves that symlink at build time into a sandbox-safe copy. Net result: **one canonical SVG in the repo**, every downstream build sees real bytes. Deps are isolated — the website is deliberately *not* a pnpm workspace member, so its lockfile and packages can't drift into the main kolu build. \`just website::{dev,check,build,nix-build,preview}\` drives local work. \`devour-flake\` already walks every flake output, so **\`just ci\`'s existing \`nix\` step builds the website automatically** — no new step, no separate context to forget. \`just ci::pnpm-hash-fresh\` now verifies the website's \`fetchPnpmDeps\` hash alongside kolu's. ## Deploy \`.github/workflows/pages.yml\` runs \`nix build .#website\` on every push to \`master\` (and this branch, for testing) that touches \`website/**\` and ships \`dist/\` to Pages via \`actions/deploy-pages@v4\`. Installation is \`nixbuild/nix-quick-install-action\`. The custom domain is configured via \`website/public/CNAME\`. ## Test plan - [x] \`nix build .#website\` produces a working static site under \`\$out/\` - [x] Dev (\`just website::dev\`) and reproducible (\`nix build\`) paths both work - [x] Light/dark toggle verified across landing and blog pages (Chrome DevTools MCP) - [x] Blog post embeds the bus-stop tweet correctly with syncing theme - [ ] Merge and DNS — \`kolu.dev\` CNAME → \`juspay.github.io\` needs to resolve before Pages serves from the custom domain 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 3672ed3 commit 9efe08a

25 files changed

Lines changed: 6088 additions & 25 deletions

.github/workflows/pages.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Build and deploy the Kolu website to GitHub Pages.
2+
#
3+
# The site lives in website/ and is exposed as packages.${system}.website
4+
# by the root flake — `nix build .#website` produces the static artefact.
5+
name: pages
6+
7+
on:
8+
push:
9+
branches: [master]
10+
paths:
11+
- "website/**"
12+
- "packages/client/favicon.svg"
13+
- ".github/workflows/pages.yml"
14+
workflow_dispatch:
15+
16+
permissions:
17+
contents: read
18+
pages: write
19+
id-token: write
20+
21+
concurrency:
22+
group: pages
23+
cancel-in-progress: false
24+
25+
jobs:
26+
build:
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- uses: nixbuild/nix-quick-install-action@v31
32+
with:
33+
nix_conf: |
34+
extra-substituters = https://cache.nixos.asia/oss
35+
extra-trusted-public-keys = oss:KO872wNJkCDgmGN3xy9dT89WAhvv13EiKncTtHDItVU=
36+
accept-flake-config = true
37+
38+
- name: Build website (nix)
39+
run: nix build .#website --print-out-paths
40+
41+
- name: Stage artefact
42+
run: |
43+
# result/ is a symlink into /nix/store (read-only). Pages needs
44+
# a writable directory with a .nojekyll file and correct perms.
45+
mkdir -p _site
46+
cp -rL result/* _site/
47+
chmod -R u+w _site
48+
touch _site/.nojekyll
49+
50+
- uses: actions/upload-pages-artifact@v3
51+
with:
52+
path: _site
53+
54+
deploy:
55+
needs: build
56+
runs-on: ubuntu-latest
57+
environment:
58+
name: github-pages
59+
url: ${{ steps.deployment.outputs.page_url }}
60+
steps:
61+
- id: deployment
62+
uses: actions/deploy-pages@v4

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66

77
A browser cockpit for coding agents. Bring your own CLI, run them anywhere.
88

9-
Unlike agent command centers that wrap a single model behind their own chat UI, kolu stays out of the agent's way: the terminal is the universal interface, so `claude`, `opencode`, `aider`, or whatever ships next week works out of the box — and you can drop to a plain shell whenever you want. It's an [Agentic Development Environment](https://x.com/jdegoes/status/2036931874057314390) (ADE) that treats terminals as the thesis, not the substrate.
9+
Unlike agent command centers that wrap a single model behind their own chat UI, kolu stays out of the agent's way: the terminal is the universal interface, so `claude`, `opencode`, or whatever ships next week works out of the box — and you can drop to a plain shell whenever you want. It's an [Agentic Development Environment](https://x.com/jdegoes/status/2036931874057314390) (ADE) that treats terminals as the thesis, not the substrate.
1010

1111
## Philosophy
1212

1313
Two principles shape what kolu is and isn't:
1414

15-
**Agent-agnostic.** The terminal is the universal interface. Kolu doesn't wrap a specific model or lock you into one CLI — `claude`, `opencode`, `aider`, or whatever ships next week all work the same way, because they're just programs you run in a shell. There's no agent registry to update, no adapter to write, no vendor lock-in. Any new agent CLI picks up first-class features automatically: run it once in any kolu terminal and the next time you create a worktree, it appears in the sub-palette as a launch option — no configuration, no per-agent code. You can always drop to a plain shell without leaving the app.
15+
**Agent-agnostic.** The terminal is the universal interface. Kolu doesn't wrap a specific model or lock you into one CLI — `claude`, `opencode`, or whatever ships next week all work the same way, because they're just programs you run in a shell. There's no agent registry to update, no adapter to write, no vendor lock-in. Any new agent CLI picks up first-class features automatically: run it once in any kolu terminal and the next time you create a worktree, it appears in the sub-palette as a launch option — no configuration, no per-agent code. You can always drop to a plain shell without leaving the app.
1616

1717
**Auto-detected, zero setup.** Kolu populates its UI by watching what you already do — the repos you `cd` into, the agents you run, the sessions you save — not by asking you to configure it. Recent repos track `cd` events, branch / PR / CI status derive from the terminal's CWD, Claude Code state is read from the foreground pid, recent agent CLIs come from preexec command marks emitted by kolu's shell integration, and activity sparklines come from pty output. If kolu knows something, it's because the shell already told it. The surface grows with your workflow, not with a preferences pane.
1818

@@ -41,7 +41,7 @@ Open http://127.0.0.1:7681 (or the address you chose above).
4141
### Navigation
4242

4343
- Command palette (<kbd>Cmd/Ctrl+K</kbd>) — search terminals, switch themes, run actions
44-
- Agent-aware command palette — once you've run a known agent CLI (`claude`, `aider`, `opencode`, `codex`, `goose`, `gemini`, `cursor-agent`) in any kolu terminal, it surfaces in two places: under `New terminal → <recent repo>` as a sub-palette that creates the worktree and launches the agent in one step, and under `Debug → Recent agents` as a prefill-into-active-terminal affordance. Prompt/message flag values (`-p`/`--prompt`/`-m`/`--message`) are stripped before storage so ephemeral prompt text never lands in the persisted MRU
44+
- Agent-aware command palette — once you've run a known agent CLI (`claude`, `opencode`, `goose`, `gemini`, `cursor-agent`) in any kolu terminal, it surfaces in two places: under `New terminal → <recent repo>` as a sub-palette that creates the worktree and launches the agent in one step, and under `Debug → Recent agents` as a prefill-into-active-terminal affordance. Prompt/message flag values (`-p`/`--prompt`/`-m`/`--message`) are stripped before storage so ephemeral prompt text never lands in the persisted MRU
4545
- Sidebar agent previews — when an agent is waiting on you (or has finished with an unread completion), its sidebar card expands with a live xterm preview so you can peek without switching. Toggle in Settings. <kbd>Ctrl+Tab</kbd> (or <kbd>Alt+Tab</kbd>) cycles terminals in MRU order: hold the modifier, press Tab to advance, release to commit
4646
- Keyboard-driven — <kbd>Cmd+T</kbd> new terminal, <kbd>Cmd+1</kbd>…<kbd>Cmd+9</kbd> jump, <kbd>Cmd+Shift+[</kbd> / <kbd>Cmd+Shift+]</kbd> cycle, <kbd>Cmd+/</kbd> shortcuts help
4747

@@ -257,6 +257,17 @@ See [`nix/home/example/`](nix/home/example/) for a full configuration with a VM
257257

258258
If kolu grows unbounded (V8 heap climbing over hours), set `services.kolu.diagnostics.dir` to an absolute path. Each restart gets its own timestamped subdir there, with a baseline heap snapshot at T+5min, periodic `"diag"` stats lines (memory bands + `terminals`/`publisherSize`/`claudeSessions`/`pendingSummaryFetches`), and automatic near-OOM snapshots via V8's `--heapsnapshot-near-heap-limit`. `kill -USR2 <pid>` captures an on-demand snapshot into the same dir. Diff two snapshots offline with [memlab](https://facebook.github.io/memlab/docs/cli/CLI-commands/) to name the retainer. Unset = zero overhead; the code path is fully gated.
259259

260+
## Website
261+
262+
The marketing site and blog at <https://kolu.dev> live in [`website/`](website/) — Astro + Tailwind, its own zero-input flake, deployed to GitHub Pages via `.github/workflows/pages.yml`.
263+
264+
```sh
265+
just website::dev # live preview with HMR
266+
just website::nix-build # reproducible build
267+
```
268+
269+
See [`website/README.md`](website/README.md) for authoring posts and deploy details.
270+
260271
---
261272

262273
Named after [கோலு](<https://en.wikipedia.org/wiki/Golu_(festival)>), the tradition of arranging figures on tiered steps.

ci/mod.just

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ _darwin-fanout: e2e
3535
_darwin:
3636
CI_SYSTEM=aarch64-darwin just ci::nix ci::_darwin-fanout || true
3737

38-
# Verify the declared pnpmDeps hash in default.nix matches what
39-
# fetchPnpmDeps actually produces from the current pnpm-lock.yaml.
38+
# Verify the declared pnpmDeps hashes match what fetchPnpmDeps produces
39+
# from the current pnpm-lock.yaml files. Covers both the main kolu lockfile
40+
# (./pnpm-lock.yaml → .#pnpmDeps) and the website lockfile
41+
# (./website/pnpm-lock.yaml → .#website-pnpm-deps).
4042
#
4143
# Two builds are required, not one: `nix build --rebuild` errors with
4244
# "outputs ... are not valid, so checking is not possible" when the store
@@ -46,10 +48,10 @@ _darwin:
4648
# or cache.nixos.asia) can't silently satisfy a hash that no longer
4749
# matches the lockfile.
4850
#
49-
# Runs on one system — hash is platform-independent (pnpm install --force
51+
# Runs on one system — hashes are platform-independent (pnpm install --force
5052
# bypasses os/cpu/libc gating, see default.nix pnpmDeps comment).
5153
pnpm-hash-fresh:
52-
just ci::_run pnpm-hash-fresh "sh -c 'nix build .#pnpmDeps --no-link && nix build --rebuild .#pnpmDeps --no-link'"
54+
just ci::_run pnpm-hash-fresh "sh -c 'nix build .#pnpmDeps .#website-pnpm-deps --no-link && nix build --rebuild .#pnpmDeps .#website-pnpm-deps --no-link'"
5355

5456
nix:
5557
just ci::_devour-flake nix --override-input flake .

flake.nix

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,24 @@
2929
{
3030
homeManagerModules.default = import ./nix/home/module.nix;
3131
packages = eachSystem (pkgs:
32-
let all = import ./default.nix { inherit pkgs commitHash; };
33-
in removeAttrs all [ "koluEnv" ]);
32+
let
33+
kolu = import ./default.nix { inherit pkgs commitHash; };
34+
# Synthesized website source tree: website/ with the canonical
35+
# favicon copied in where the working tree has a symlink to
36+
# ../../packages/client/favicon.svg. One SVG on disk; the Nix
37+
# sandbox still sees a self-contained website/ with real bytes.
38+
websiteSrc = pkgs.runCommand "kolu-website-src" { } ''
39+
cp -r ${./website} $out
40+
chmod -R u+w $out
41+
rm -f $out/public/favicon.svg
42+
cp ${./packages/client/favicon.svg} $out/public/favicon.svg
43+
'';
44+
website = import ./website { inherit pkgs; src = websiteSrc; };
45+
in
46+
removeAttrs kolu [ "koluEnv" ] // {
47+
website = website.default;
48+
website-pnpm-deps = website.pnpmDeps;
49+
});
3450
devShells = eachSystem (pkgs:
3551
let default = import ./shell.nix { inherit pkgs; };
3652
in {

justfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ cucumber_parallel := env('CUCUMBER_PARALLEL', '4')
1414

1515
mod ai 'agents/ai.just'
1616
mod ci 'ci/mod.just'
17+
mod website 'website/mod.just'
1718

1819
# List available recipes
1920
default:
@@ -97,11 +98,11 @@ clean:
9798

9899
# Format all files in-place
99100
fmt: install
100-
{{ nix_shell }} sh -c 'pnpm exec prettier --write --cache --ignore-unknown . && nixpkgs-fmt *.nix nix/**/*.nix'
101+
{{ nix_shell }} sh -c 'pnpm exec prettier --write --cache --ignore-unknown . && nixpkgs-fmt *.nix nix/**/*.nix website/*.nix'
101102

102103
# Check formatting without modifying files (used by CI)
103104
fmt-check: install
104-
{{ nix_shell }} sh -c 'pnpm exec prettier --check --cache --ignore-unknown . && nixpkgs-fmt --check *.nix nix/**/*.nix'
105+
{{ nix_shell }} sh -c 'pnpm exec prettier --check --cache --ignore-unknown . && nixpkgs-fmt --check *.nix nix/**/*.nix website/*.nix'
105106

106107
# Nix build (server + client)
107108
build:

packages/client/public/favicon.svg

Lines changed: 0 additions & 8 deletions
This file was deleted.

packages/client/public/favicon.svg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../favicon.svg

website/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
dist/
2+
.astro/
3+
node_modules/
4+
.env
5+
.env.production
6+
.DS_Store
7+
*.log

website/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# kolu website
2+
3+
Marketing + blog for [kolu](https://github.com/juspay/kolu). Astro + Tailwind
4+
v4, deployed to <https://kolu.dev> via GitHub Pages.
5+
6+
## Develop
7+
8+
```sh
9+
just website::dev # HMR on http://127.0.0.1:4321
10+
just website::nix-build # reproducible Nix build → /nix/store/...
11+
```
12+
13+
Blog posts: `src/content/blog/*.md` (schema in `src/content.config.ts`).
14+
Frontmatter `title`, `description`, `pubDate`, optional `author` +
15+
`authorUrl`. Don't include a leading `# ` heading — it comes from the
16+
frontmatter `title`.
17+
18+
## Deploy
19+
20+
`.github/workflows/pages.yml` runs `nix build .#website` on every push to
21+
`master` that touches `website/**` and publishes the result. `just ci`
22+
builds the site too (devour-flake walks the root flake's outputs).
23+
24+
## Update deps
25+
26+
Bumping `pnpm-lock.yaml` changes the `fetchPnpmDeps` hash in
27+
`default.nix`. `just ci::pnpm-hash-fresh` verifies both the kolu and
28+
website hashes — paste the printed hash back in on mismatch.

website/astro.config.mjs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// @ts-check
2+
import { defineConfig } from "astro/config";
3+
import mdx from "@astrojs/mdx";
4+
import sitemap from "@astrojs/sitemap";
5+
import tailwindcss from "@tailwindcss/vite";
6+
7+
// https://astro.build/config
8+
// Port pinned to 4321 (Astro default) — kept explicit to make clear it
9+
// never collides with Kolu's default 7681.
10+
const DEV_PORT = 4321;
11+
12+
export default defineConfig({
13+
site: "https://kolu.dev",
14+
trailingSlash: "ignore",
15+
server: { port: DEV_PORT, host: "127.0.0.1" },
16+
integrations: [mdx(), sitemap()],
17+
vite: {
18+
plugins: [tailwindcss()],
19+
},
20+
markdown: {
21+
shikiConfig: {
22+
// Dual theme — astro emits both as CSS variables; global.css routes
23+
// them via `[data-theme]` so code blocks track the light/dark toggle.
24+
themes: {
25+
light: "vitesse-light",
26+
dark: "vitesse-black",
27+
},
28+
defaultColor: false,
29+
wrap: false,
30+
},
31+
},
32+
});

0 commit comments

Comments
 (0)