Brief for AI agents working in this repository. Read this first.
bZ's music site at https://music.megabyte.space. Vanilla TypeScript + Vite bundle served by a Cloudflare Worker (worker/index.ts). The Worker handles SEO rewriting, the /api/* surface, and edge caching for /audio/* + /lyrics/*.
The app is a single-page experience. The audio element is created once and persists across all internal navigation — never tear it down on a route change.
- Frontend: vanilla TypeScript + Vite v6, no UI framework
- Backend: Cloudflare Worker + Wrangler v4
- State: KV namespace
COUNTERS(plays, shares, rate limits, push subs, listmonk cache) - Tests: Playwright v1.59 E2E, 6 breakpoints, runs against
PROD_URL - Deploy:
npx wrangler deploythen purge the CF zone
| Task | File |
|---|---|
| New track or album | src/data.ts |
| Per-route SEO / OG tags | src/track-meta.ts (consumed by worker/index.ts) |
| Transport UI / app shell | src/main.ts (large; search first) |
| AI DJ drawer | src/ai-chat.ts |
| AI chat widget kinds + renderer | src/ai-widgets.ts (see docs/ai-chat-widgets.md) |
/shortcommands palette builder |
src/ai-shortcommands.ts (see docs/ai-chat-commands.md) |
| Visualizer | src/visualizer.ts |
| Worker route or API | worker/index.ts |
| Styles | src/style.css (one file, cascade-layered) |
- Audio persistence.
<audio data-engine="bz">is created insrc/audio.tsand owned by the top-level document. Any change that tears it down on navigation is a regression. - SPA-only routing. Routes like
/canopy/<track>,/halo/...,/appeal,/ashtonare served byenv.ASSETS.fetch(new URL('/', ...))so the same shell boots. Don't add a real HTML file at those paths. - Per-route metadata is server-rendered. Add metadata in
src/track-meta.tssoMetaRewriterinworker/index.tsrewrites<head>at the edge. Never inject<title>/<meta>from client-side JS for crawlable content. - TypeScript strict is on.
tsc -bruns innpm run build. Don't disable strict; add types instead. - Tests start at the homepage. Real-user flows — click through to the feature; don't
page.gotoan internal route unless the test specifically asserts per-route SEO.
npm run dev # local dev (vite, http://localhost:5173)
npm run build # tsc -b && vite build (regenerates OG cards via prebuild)
npm run test:e2e # Playwright against PROD_URL
npx wrangler deploy # ship the worker + assetsAfter deploy: purge the CF zone (/client/v4/zones/<zone>/purge_cache) so the rewritten HTML hits clients immediately.
Worker reads secrets from wrangler secret put:
ANTHROPIC_API_KEY— required for/api/ai/chatLISTMONK_API_TOKEN— required for/api/subscribeVAPID_*+PUSH_ADMIN_TOKEN— required for/api/push/*STRIPE_SECRET_KEY+STRIPE_WEBHOOK_SECRET— required for/api/merch/*
Local dev uses .dev.vars (gitignored).
- Payments: Stripe for ALL online payments — always.
/api/merch/checkoutis Stripe Checkout and stays that way. Square is only ever added when a real in-person/POS leg exists (this storefront has none). Do not migrate online flows to Square. - Lyrics timing: if karaoke is "off", the stored lyrics are usually the wrong Suno take (or polluted with prompt metadata), NOT a timing bug — the aligner's
stats.matchRatewill be low. Rebuild from the accurate transcript withnpm run lyrics:rebuild -- <id>(wrapsscripts/rebuild-lyrics-from-cache.mjs), then it re-syncssrc/data.ts.npm run lyrics:validate(also inprebuild) hard-fails on wrong-take (matchRate <60%) + collapsed lines; allowlist a track in that script only when Whisper genuinely can't hear it (e.g.cbo-pennumber-rap). - Lyrics: Christian-gangster ethic. Zero drug references. See
~/.claude/projects/-Users-apple-emdash-projects-music-megabyte-space/memory/feedback_lyrics_christian_gangster.md. - Cast: default receiver (
CC1AD845) until the custom receiver is device-bound. Custom App ID228565CBlives insrc/cast-protocol.ts. - Karaoke overlay: word highlighting mirrors the fullscreen lyrics pattern in
src/main.ts(parallel arrayskaraokeOverlayWords+karaokeOverlayWordSpans+karaokeOverlayWordIdx, driven by the existingstartKaraoketick loop). - AI chat widgets: assistant messages can carry typed
AiChatWidget[]payloads — track-card, album-card, command-palette, citation, alert, code-snippet, etc. The renderer insrc/ai-widgets.tsis a pure HTML-string builder; every URL goes throughsafeUrl()and every string throughescapeHtml(). Run/shortcommands(or/sc) in the chat panel to see every slash command as a grouped, clickable palette. Full reference:docs/ai-chat.md,docs/ai-chat-widgets.md,docs/ai-chat-commands.md.
- Don't add a UI framework (React/Vue/Angular). The project is intentionally vanilla.
- Don't split
src/style.cssinto many files; the cascade-layer order is load-bearing. - Don't add a database or auth provider. The site is stateless beyond KV counters.
- Don't introduce ESLint with autofix in the same PR as feature work — the existing source has not been linted; do it once, separately.