Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cc038b9
fix(analytics): forward $raw_user_agent and $host for cookieless inge…
biggest-littlest Aug 15, 2026
985bd2a
chore(git): reconcile main before cookieless promotion
scttbnsn Aug 15, 2026
3b6d9cd
chore(config): remove the stale Cursor rules (#54)
scttbnsn Aug 16, 2026
30cdd8e
docs(readme): describe what this repo is and how it deploys (#55)
scttbnsn Aug 16, 2026
89bad5b
chore(git): reconcile main before docs promotion
scttbnsn Aug 16, 2026
d8aec70
chore(gitignore): ignore the root .vercel link and history-backup bun…
scttbnsn Aug 26, 2026
f50fadb
feat(analytics): capture $pageleave and send $pathname (#60)
scttbnsn Aug 26, 2026
1a40882
fix(seo): repair JSON-LD logo 404 and double-slash base URLs
scttbnsn Aug 28, 2026
e1f7a85
build(deps): bump next to ^16.2.11 to clear all 35 Dependabot alerts
scttbnsn Aug 28, 2026
ac19fbb
build(deps): regenerate next-env.d.ts for next 16.3
scttbnsn Aug 28, 2026
faea446
docs(roadmap): track web-analytics table coverage follow-ups (ops X37)
scttbnsn Aug 28, 2026
25e49cd
chore: ignore .claude/ with a tracked line (#59)
scttbnsn Aug 28, 2026
9025b2f
docs(roadmap): point acquisition-data item at the ops analytics standard
scttbnsn Aug 28, 2026
f4add91
chore(git): reconcile main before analytics and hardening promotion
scttbnsn Aug 28, 2026
5aade3b
docs(roadmap): pageleave ratio is structural; note the bot-detection …
scttbnsn Aug 28, 2026
b34f329
chore(config): add .planning/ to .gitignore (#62)
scttbnsn Sep 7, 2026
b620263
chore(git): reconcile main before the .planning gitignore promotion
scttbnsn Sep 7, 2026
2d3ccab
fix(analytics): preserve validated referring hostnames (#64)
scttbnsn Sep 13, 2026
16c153c
chore(git): reconcile main before the referrer reporting promotion
scttbnsn Sep 13, 2026
4493004
fix(site): restore hydration and reject invalid signup bodies (#66)
scttbnsn Sep 13, 2026
cac45c0
chore(git): reconcile main before the hydration repair promotion
scttbnsn Sep 13, 2026
14478e5
feat(analytics): add project, newsletter and client error events with…
scttbnsn Oct 22, 2026
488e736
feat(site): ship the Catalogue redesign with a support page (#69)
scttbnsn Oct 22, 2026
410e3d5
chore(git): reconcile main before the Catalogue promotion
scttbnsn Oct 22, 2026
cc9da03
fix(site): address promotion review on theme fallback, support meta a…
scttbnsn Oct 22, 2026
ccf6b86
build(deps): patch next, dompurify and sharp security advisories (#72)
scttbnsn Oct 25, 2026
8cb1156
ci(website): harden the verify workflow, remove stray files, refresh …
scttbnsn Oct 25, 2026
e422e9a
chore(git): reconcile main before the security patch promotion
scttbnsn Oct 25, 2026
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
20 changes: 17 additions & 3 deletions .github/workflows/website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ on:
branches:
- main
- dev
- "feature/**"
pull_request:
branches:
- main
Expand All @@ -14,18 +13,29 @@ on:
permissions:
contents: read

concurrency:
# PR runs share a group per PR so a superseded head is cancelled. Push runs get
# a unique group so a queued dev/main run is never replaced by a newer push;
# the promotion skip relies on every dev SHA keeping its own verify run.
group: website-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
verify:
# Only this repository's dev-to-main promotion has a matching dev push run.
if: github.event_name != 'pull_request' || github.head_ref != 'dev' || github.base_ref != 'main' || github.event.pull_request.head.repo.id != github.event.repository.id
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: frontend
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24.x
cache: npm
Expand All @@ -36,10 +46,14 @@ jobs:
run: npm run lint
- name: Type-check
run: npm run type-check
- name: Verify workflow event boundaries
run: node --test test/workflow.test.mjs
- name: Run scene tests
run: npm run test:scene
- name: Run PostHog contracts
run: npm run test:posthog
- name: Run support contracts
run: npm run test:support
- name: Build production app
run: npm run build
- name: Install browser for production checks
Expand Down
106 changes: 65 additions & 41 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,57 @@
Loose, personal roadmap for the CodesWhat homepage. Not a ticket tracker (that's
work). Just the ideas worth not forgetting.

## Progressive-enhancement fallback for the 3D layer
## Where things stand (2026-10-25)

The Catalogue redesign, the `/support` page and the analytics events shipped on
2026-10-22 (#69, #70, #71). Production is `main@68520ee`. Two things are in
flight:

The homepage renders its coin, mascot balloons, and the `@` marble live in WebGL
(three.js + react-three-fiber). That buys real interactivity: the balloons track
scroll and dodge the cursor, the marble orbits and flees. The cost isn't mainly
bandwidth (the three/fiber/drei bundle is ~300KB gzipped, loaded once and cached,
and the texture PNGs load either way). The cost is compute: WebGL burns CPU/GPU
and battery and adds parse + hydration time, which hurts on cheap phones.
- [ ] Dependency security patch (Next 16.4.3, dompurify, sharp): PR #72 is green
on CI and preview and waiting on a delivered review before it lands in `dev`,
then a promotion to `main`.
- [ ] CI hardening (support tests in `verify`, no duplicate promotion runs,
fork-safe skip, no persisted checkout credential, actions pinned to
commits): finished and tested locally, queued behind #72.
- [ ] Remove the stray `visual-schedule-manager-portfolio/` app and root
`start.sh`; nothing references either.
- [ ] Clean up `test/posthog.test.ts`, which imports a posthog-js internal path,
then bump `posthog-js` and add `test:posthog:browser` to CI.
- [ ] Close #58 (`.claude/` is already ignored on `main`). #57 is partly done;
the remaining items are tracked privately.

## Progressive-enhancement fallback for the 3D layer

Baked assets (the WebP loops in `exports/`) are the opposite: nearly free to
render, but dumb. They can't track scroll or react to the pointer.
**Superseded 2026-10-22.** The Catalogue redesign replaced the WebGL homepage
with CSS and pixel-sprite specimen plates (`components/catalogue/sprites.ts`).
three.js, react-three-fiber, drei and rapier are now only imported under
`components/studio/`, which backs the dev-only `/studio/*` capture routes. There
is no 3D layer on the shipped homepage to gate or fall back from, so the original
checklist below is moot and kept for the reasoning only.

So the move for a lighter homepage isn't ripping WebGL out. It's a fallback:
serve flat assets by default and upgrade to WebGL when the device can handle it,
or when `prefers-reduced-motion` is off. That's more code, not less, but it gives
weak devices and reduced-motion users a cheap, static homepage while everyone
else still gets the interactive scene.
The original idea: the homepage rendered its coin, mascot balloons and the `@`
marble live in WebGL. That bought real interactivity (balloons tracking scroll and
dodging the cursor, the marble orbiting and fleeing) at the cost of compute,
battery and hydration time on cheap phones. Baked WebP loops were nearly free to
render but couldn't react to anything. The plan was to serve flat assets by
default and upgrade to WebGL behind `prefers-reduced-motion` and a capability
check, lazy-loading the 3D bundle only on the upgrade path, and to measure First
Load JS and TTI for both paths rather than guess.

- [ ] Static fallback layer (coin WebP + balloon stills positioned to match the scene)
- [ ] Gate WebGL behind `prefers-reduced-motion` + a capability/perf check
- [ ] Lazy-load the 3D bundle only when the upgrade path is taken
- [ ] Measure: real First Load JS + TTI for both paths (build, don't guess)
- [ ] Decide whether the three/fiber/drei/rapier dependencies stay for the
`/studio` capture rig or go. They're dev-only but still in `package.json`
and the lockfile, so they show up in audits (`fflate` moderate via drei).

## Coherent 3D brand-asset / icon set

`scripts/capture-assets.mjs` mints transparent assets straight from the live scene
(dev-only `/studio/*` routes → headless Chrome → ffmpeg/img2webp). It already does
the coin (spin) and any mascot balloon (square avatar still + spin). Make the whole
icon/image story coherent off the back of it:
**Parked 2026-10-25.** The Catalogue uses hand-drawn pixel sprites for all eight
projects plus the coin, matched to the real project logos, so the 3D-minted
icon set isn't on the critical path any more. `scripts/capture-assets.mjs` and
the `/studio/*` routes still work and still mint the coin spin and any mascot
balloon (square avatar still + spin). What exists today: `exports/coin/`
(light and dark spin WebP) and `exports/portkey/` (avatar PNG + spin WebP), plus
static balloon PNGs for drydock, portkey, portwing, sockguard and scott in
`frontend/public/logos/`.

- [ ] Avatar + spin for every mascot (Sockguard, Drydock, Portwing, Portkey), not just Portkey
- [ ] Decide where committed assets live and how they're named (currently `exports/<subject>/`, WebP + still PNG tracked, GIF/APNG gitignored)
Expand All @@ -40,26 +62,25 @@ icon/image story coherent off the back of it:

## Web-analytics table coverage (2026-08-28, ops X37)

Production events carry no `$pathname` and no `$pageleave`, so PostHog's Page /
Entry / Exit tables render empty and every session counts as a zero-duration
bounce. The wiring for both landed on dev as PR #60 (sanitizer allowlists
`$pageleave` and stamps `$pathname` from the already-sanitized `path`) and
ships with the next promotion — the items below are what's left after that.
Production events carried no `$pathname` and no `$pageleave`, so PostHog's Page /
Entry / Exit tables rendered empty and every session counted as a zero-duration
bounce. The wiring for both landed as PR #60 and has been in production since the
September promotions.

- [ ] Verify post-deploy that `$pathname` shows on fresh pageviews and the
Page/Entry/Exit tables populate (board: CodesWhat Sites Health,
project 558033, dashboard 2044260)
- [ ] `$pageleave` volume note (resolved 2026-08-28, standard `19a0af1`): a
- [x] Verified 2026-10-25 against PostHog project 558033 (host `codeswhat.com`,
last 30 days): 30 `$pageview`, all 30 carrying `$pathname`; 9 `$pageleave`;
22 `$web_vitals`. The Page/Entry/Exit tables have data to render.
- [x] `$pageleave` volume note (resolved 2026-08-28, standard `19a0af1`): a
pageleave-to-pageview ratio well under 100% is structural, not a bug —
`$pageview` fires per client-side route change, `$pageleave` once per
document lifetime. drydock measured the envelope: the sanitizer guard
eats nothing. Don't "fix" the ratio here.
- [ ] Post-deploy canary caveat: posthog-js bot detection (`isLikelyBot`
checks `navigator.webdriver` and `userAgentData.brands`) may silently
drop Playwright-driven visits before `before_send` runs; contested
(portwing's landed), so a Playwright canary's silence proves nothing
either way. Verify with a real browser or by reading the deployed chunk
for the property names.
- [x] Post-deploy canary caveat: moot now that real traffic confirms the
properties. For the record, posthog-js bot detection (`isLikelyBot` on
`navigator.webdriver` and `userAgentData.brands`) may silently drop
Playwright-driven visits before `before_send` runs, so a Playwright canary's
silence proves nothing either way. Verify with a real browser or by reading
the deployed chunk.
- [x] Acquisition data: decided org-wide in `CodesWhat/ops`
`standards/analytics.md` ("Acquisition data and consent", as of
`c161ebc`) — that file is the authority. Landed: `save_campaign_params:
Expand All @@ -74,10 +95,13 @@ ships with the next promotion — the items below are what's left after that.
(cookieless strips the IP upstream, PostHog #48660). No banner needed.
- [x] `$web_vitals` carries `$pathname` from the sanitized path
- [x] `cta activated` for the project cards: `project_<slug>` ids, placement
`projects`. `cta activated` had zero rows in 120 days on codeswhat.com;
reproduced in a browser and the capture, sanitizer and send path all work,
so it was low traffic (76 pageviews) rather than a bug. Check it fires after
deploy.
`projects`. Shipped 2026-10-22 (#68/#70). Capture, sanitizer and send path
were reproduced in a browser before shipping.
- [ ] Production fire check for `cta activated`, `newsletter submitted` and
`client error`: none of the three has a row for host `codeswhat.com` in the
30 days to 2026-10-25. With 30 pageviews in that window that's low traffic,
not evidence of a bug, so leave this open until one lands or a real-browser
click on production is checked in PostHog.
- [x] `newsletter submitted` (`result`, plus `error_class` from
invalid_email / rate_limited / unavailable / network on error)
- [x] `client error` (`error_kind`, allowlisted `error_name`, sanitized path;
Expand Down
Loading
Loading