From 869eb1a2846426cfda3897e4f8318a869c8ab4f4 Mon Sep 17 00:00:00 2001 From: dommango-sys <251805093+dommango@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:05:37 -0400 Subject: [PATCH 1/2] docs: site audit (2026-08-31) and ten executable improvement plans Audit ranks changes by value to visitors and records four verified P0s: contact form unconfigured in the Pages build, Substack 403 from Actions hiding the Writing section, a keyword-matcher assistant, and the default 404 for pre-redesign URLs. Each plan in docs/plans/ is self-contained. --- docs/plans/01-reconnect-live-plumbing.md | 170 +++++++++ docs/plans/02-project-case-studies.md | 327 ++++++++++++++++++ .../03-writing-featured-and-subscribe.md | 274 +++++++++++++++ docs/plans/04-social-preview-card.md | 141 ++++++++ docs/plans/05-now-strip.md | 252 ++++++++++++++ docs/plans/06-honest-assistant.md | 229 ++++++++++++ docs/plans/07-not-found-rescue.md | 157 +++++++++ docs/plans/08-contrast-and-a11y-pass.md | 208 +++++++++++ docs/plans/09-travel-scrubber-and-touch.md | 214 ++++++++++++ docs/plans/10-repo-hygiene.md | 158 +++++++++ docs/plans/README.md | 87 +++++ 11 files changed, 2217 insertions(+) create mode 100644 docs/plans/01-reconnect-live-plumbing.md create mode 100644 docs/plans/02-project-case-studies.md create mode 100644 docs/plans/03-writing-featured-and-subscribe.md create mode 100644 docs/plans/04-social-preview-card.md create mode 100644 docs/plans/05-now-strip.md create mode 100644 docs/plans/06-honest-assistant.md create mode 100644 docs/plans/07-not-found-rescue.md create mode 100644 docs/plans/08-contrast-and-a11y-pass.md create mode 100644 docs/plans/09-travel-scrubber-and-touch.md create mode 100644 docs/plans/10-repo-hygiene.md create mode 100644 docs/plans/README.md diff --git a/docs/plans/01-reconnect-live-plumbing.md b/docs/plans/01-reconnect-live-plumbing.md new file mode 100644 index 0000000..6770548 --- /dev/null +++ b/docs/plans/01-reconnect-live-plumbing.md @@ -0,0 +1,170 @@ +# Plan 01 — Reconnect the live plumbing + +**Priority:** P0 (do first) · **Effort:** ~2–3 hours · **Value:** restores the two conversion paths that are dead today +**Depends on:** nothing · **Unblocks:** plans 03, 05 + +## Why + +Three things are broken on https://dommango.github.io right now. All three were verified on 2026-08-31, not inferred: + +1. **The contact form cannot send.** `lib/services/emailjs.ts` reads `NEXT_PUBLIC_EMAILJS_*` at build time. `.github/workflows/deploy.yml` passes **no** environment variables to `npm run build`, so the deployed bundle contains `"Email service not configured"` and no EmailJS service id. Every visitor who submits the form gets that error. (Checked by downloading the live chunks and grepping.) +2. **The Writing section never appears.** `scripts/fetch-substack.js` runs during the deploy build, and Substack answers GitHub Actions with HTTP 403. The job log for run 33308370829 says: `[substack] feed returned 403; keeping committed posts`. The committed `POSTS` array is empty, so the section (and its nav link) stay hidden even though a real post ("The game had already started", 2026-08-04) exists. +3. **No analytics are collected.** `NEXT_PUBLIC_GOATCOUNTER_SITE` is not in the build env, so `app/layout.tsx` never renders the GoatCounter script. `public/data/analytics.json` has been zeros since March. (The `GOATCOUNTER_SITE` secret already exists in the repo; it's just not wired to the build.) + +A fourth P0 — the chat widget's wrong answers — is fixed properly in plan 06. This plan includes a **one-line stopgap** so the widget stops misinforming people today. + +## Done when + +- [ ] Sending the live contact form delivers an email to Dom's inbox (not to the sender). +- [ ] https://dommango.github.io shows a **Writing** section with the Aug 4 post and a **Writing** nav link. +- [ ] `curl -s https://dommango.github.io | grep -c goatcounter` prints `1` or more. +- [ ] The chat bubble is not rendered on the live site until plan 06 ships. +- [ ] The deploy workflow **fails loudly** if any required build secret is missing, so this can't silently regress. + +## Files + +- `.github/workflows/deploy.yml` — pass secrets into the build; add a guard step +- `lib/content/writing.ts` — regenerated by the fetch script (commit the result) +- `scripts/fetch-substack.js` — add a browser-like User-Agent and one fallback endpoint (best-effort) +- `app/layout.tsx` — don't render `` when no chat API is configured +- `scripts/check-build-env.js` — new, tiny + +## Steps + +### 1. Put the EmailJS + reCAPTCHA values into GitHub secrets + +The values live in `.env.local` (gitignored). Never paste them into a chat or a commit. Run this from the repo root; it reads the file and pipes each value straight into `gh` without echoing: + +```bash +for k in NEXT_PUBLIC_EMAILJS_SERVICE_ID NEXT_PUBLIC_EMAILJS_TEMPLATE_ID NEXT_PUBLIC_EMAILJS_PUBLIC_KEY NEXT_PUBLIC_RECAPTCHA_SITE_KEY; do + v=$(grep "^$k=" .env.local | cut -d= -f2-) + if [ -n "$v" ]; then printf '%s' "$v" | gh secret set "$k"; echo "set $k"; else echo "SKIP $k (empty)"; fi +done +gh secret list +``` + +`GOATCOUNTER_SITE` already exists as a secret; reuse it below. + +### 2. Wire the secrets into the build (`.github/workflows/deploy.yml`) + +Replace the `Build Next.js` step with a guard + build: + +```yaml + - name: Check required build env + env: + NEXT_PUBLIC_EMAILJS_SERVICE_ID: ${{ secrets.NEXT_PUBLIC_EMAILJS_SERVICE_ID }} + NEXT_PUBLIC_EMAILJS_TEMPLATE_ID: ${{ secrets.NEXT_PUBLIC_EMAILJS_TEMPLATE_ID }} + NEXT_PUBLIC_EMAILJS_PUBLIC_KEY: ${{ secrets.NEXT_PUBLIC_EMAILJS_PUBLIC_KEY }} + run: node scripts/check-build-env.js + + - name: Build Next.js + env: + NEXT_PUBLIC_EMAILJS_SERVICE_ID: ${{ secrets.NEXT_PUBLIC_EMAILJS_SERVICE_ID }} + NEXT_PUBLIC_EMAILJS_TEMPLATE_ID: ${{ secrets.NEXT_PUBLIC_EMAILJS_TEMPLATE_ID }} + NEXT_PUBLIC_EMAILJS_PUBLIC_KEY: ${{ secrets.NEXT_PUBLIC_EMAILJS_PUBLIC_KEY }} + NEXT_PUBLIC_RECAPTCHA_SITE_KEY: ${{ secrets.NEXT_PUBLIC_RECAPTCHA_SITE_KEY }} + NEXT_PUBLIC_GOATCOUNTER_SITE: ${{ secrets.GOATCOUNTER_SITE }} + run: npm run build +``` + +Create `scripts/check-build-env.js` (CommonJS, like `fetch-substack.js`): + +```js +// Fails the deploy build when a NEXT_PUBLIC_* value the site needs is missing. +// Local builds don't run this (see deploy.yml), so `npm run build` still works +// without a .env.local. +const REQUIRED = [ + 'NEXT_PUBLIC_EMAILJS_SERVICE_ID', + 'NEXT_PUBLIC_EMAILJS_TEMPLATE_ID', + 'NEXT_PUBLIC_EMAILJS_PUBLIC_KEY', +] + +const missing = REQUIRED.filter((k) => !process.env[k]) +if (missing.length > 0) { + console.error(`[env] missing required build env: ${missing.join(', ')}`) + console.error('[env] set them with `gh secret set ` — see docs/plans/01-reconnect-live-plumbing.md') + process.exit(1) +} +console.log('[env] all required build env present') +``` + +`ci.yml` does **not** run this step and should not — CI builds without secrets on purpose. + +### 3. Verify the EmailJS template sends to Dom, not the visitor + +`sendContactEmail` passes `to_name: fromName, to_email: fromEmail`. Whether that matters depends on the EmailJS template: if the template's "To email" field is `{{to_email}}`, the message goes to the **sender**. Open the template in the EmailJS dashboard and make sure "To email" is Dom's address (hard-coded), and `reply_to` is `{{reply_to}}`. Then remove the misleading params from `lib/services/emailjs.ts`: + +```ts + { + from_name: fromName, + from_email: fromEmail, + reply_to: fromEmail, + message + } +``` + +### 4. Make the Writing section appear — commit the posts + +Substack blocks GitHub's IP range, but not a residential connection. From the repo root: + +```bash +node scripts/fetch-substack.js # expect: [substack] wrote 1 post(s) to lib/content/writing.ts +git diff lib/content/writing.ts # POSTS now has one entry +npm test -- --run # 13 tests pass +npx playwright test e2e/landing.spec.ts -g "writing section" # asserts section + nav link both present +``` + +Commit `lib/content/writing.ts`. This is the reliable path: the committed posts are what ship whenever the CI fetch fails, which is currently always. + +**Recurring:** after each new post is published, run the two commands above and commit. (If `/content-publish` is used, add these two lines to the end of that skill so it happens automatically.) + +### 5. Make the CI fetch a little more likely to succeed (best-effort) + +In `scripts/fetch-substack.js`, change the request headers and add one fallback. Keep every existing guard. + +```js +const FEED_URLS = [ + 'https://dommangonon.substack.com/feed', + // Substack's JSON API sometimes answers when the RSS route is challenged. + 'https://dommangonon.substack.com/api/v1/posts?limit=6', +] +const HEADERS = { + 'user-agent': 'Mozilla/5.0 (compatible; dommango.github.io build; +https://dommango.github.io)', + accept: 'application/rss+xml, application/xml, application/json;q=0.9, */*;q=0.8', +} +``` + +Loop over `FEED_URLS`; for the JSON endpoint map each item to `{ title, url: canonical_url, date: post_date, subtitle }` and apply the same placeholder filter (`title` of "Coming soon"). If both fail, keep the existing `keeping committed posts` behaviour. Add one unit test in `__tests__/parse-substack-feed.test.ts` only if you extract the JSON mapping into `scripts/lib/parse-substack-json.js`; otherwise no new tests — this path is best-effort by design. + +### 6. Stopgap for the chat widget (`app/layout.tsx`) + +```tsx +const CHAT_API_URL = process.env.NEXT_PUBLIC_CHAT_API_URL +... + {children} + {CHAT_API_URL && } +``` + +With no API configured the widget — and its "Skills page" answers — disappears. Plan 06 replaces it. + +### 7. Deploy and verify + +```bash +git checkout -b fix/reconnect-live-plumbing +git add -A && git commit -m "fix: pass build secrets to the Pages deploy, commit Substack posts, hide unconfigured chat" +git push -u origin fix/reconnect-live-plumbing +gh pr create --fill +# after merge: +gh run watch # deploy.yml should pass the new "Check required build env" step +curl -s https://dommango.github.io | grep -c 'id="writing"' # 1 +curl -s https://dommango.github.io | grep -c goatcounter # >= 1 +``` + +Then send a real message through the live form and confirm it arrives in Dom's inbox. Check the dashboard at `/dashboard-m7x9k2` two days later: page views should be non-zero. + +## Gotchas + +- `NEXT_PUBLIC_*` values are inlined **at build time**. Setting a secret without re-running the deploy changes nothing; trigger `gh workflow run deploy.yml` after setting them. +- The reCAPTCHA site key is public by design (it's shipped in the page) but keep it in secrets anyway so the workflow reads one source. +- Don't add these env vars to `ci.yml`. CI must keep building without secrets so pull requests from forks work. +- `writing.ts` between the `GENERATED` markers is machine-written; never hand-edit inside the markers. diff --git a/docs/plans/02-project-case-studies.md b/docs/plans/02-project-case-studies.md new file mode 100644 index 0000000..31f0b45 --- /dev/null +++ b/docs/plans/02-project-case-studies.md @@ -0,0 +1,327 @@ +# Plan 02 — Project case studies with real screenshots + +**Priority:** P1 (highest value) · **Effort:** ~1 day + collecting screenshots · **Value:** very high +**Depends on:** nothing (plan 03 links into this) · **Mock-up:** section 02 of the mock-ups board + +## Why + +The page "leads with the project portfolio" and contains no image of any project. The cards link out, but: + +- **Bracketeer** → `fifawc26.up.railway.app` is a sign-in wall. A visitor sees "Sign in or create an account" and nothing else. +- **SousIQ** → a waitlist landing page. Fine, but it isn't the product. +- **PRIAL Pipeline** → no link at all. **modular-mind** → a GitHub file listing. + +A hiring manager or fellow builder cannot *see* what was shipped. Each card gets a thumbnail, and each project gets an in-page case study — problem → what I built → what broke → outcome — with a facts strip and links (live, source, related post). The project data stays hand-authored TypeScript in `lib/content/projects.ts`, per CLAUDE.md. + +## Done when + +- [ ] Every card has a 16:10 image (real screenshot, or a rendered "data" tile for pipeline projects). +- [ ] Clicking/activating a card opens its case study below the grid; clicking again closes it. Only one open at a time. +- [ ] Case study is keyboard-operable (`Enter`/`Space` on the card, tabs by arrow keys optional) with `aria-expanded` / `aria-controls`. +- [ ] Cards still expose an external link (live / source) as a real `` with `target="_blank" rel="noreferrer"`. +- [ ] Works in all three themes and at 320px wide with no horizontal scroll (existing e2e guards this). +- [ ] `npx tsc --noEmit`, `npm test -- --run`, `npx playwright test` all pass. + +## Files + +- `lib/content/projects.ts` — extend the `Project` type and data +- `components/landing/Projects.tsx` — thumbnails, expandable state, panel slot +- `components/landing/CaseStudy.tsx` — new +- `app/globals.css` — new classes; add grids to the `@media (max-width: 900px)` block +- `public/projects/*.webp` — images +- `e2e/landing.spec.ts` — update the "linked project cards" test, add a case-study test + +## Steps + +### 1. Collect the images (`public/projects/`) + +Target 1280×800 source, exported as WebP ≤ 120 KB, named by project id slug: + +| File | Source | +|---|---| +| `sousiq.webp` | Screenshot of the SousIQ app (a logged-in invoice → line items screen is far better than the landing page). Dom captures this; the landing page is the fallback. | +| `bracketeer.webp` | A logged-in leaderboard or bracket screen. Fallback: the Substack post's cover image (the original HTML bracket) — download `https://substackcdn.com/image/fetch/w_1280,c_limit,f_webp,q_auto:good/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1803b868-ba18-43a3-93d9-7fe96d55c7de_1440x900.png`. | +| `placemat.webp` | `https://dommango.github.io/claude-code-placemat/` — dismiss the "What's new" modal first. | +| `modular-mind.webp` | A rendered VCV Rack patch from the repo's `render-service`, or the repo README hero if one exists. Do **not** use a GitHub file listing. | +| `prial.webp` | No UI exists. Render a data tile instead (step 2) — no image file. | + +A capture helper, if useful (needs a logged-in `storageState` for the private apps — capture those manually): + +```js +// scripts/capture-project-shots.mjs (run: node scripts/capture-project-shots.mjs) +import { chromium } from 'playwright' +const shots = [ + ['placemat', 'https://dommango.github.io/claude-code-placemat/'], +] +const browser = await chromium.launch() +for (const [name, url] of shots) { + const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }) + await page.goto(url, { waitUntil: 'networkidle' }) + await page.keyboard.press('Escape') + await page.screenshot({ path: `public/projects/${name}.png` }) + await page.close() +} +await browser.close() +``` + +Convert PNG → WebP with `cwebp -q 80 in.png -o out.webp` (install `webp` via apt) or any image tool. Delete the PNGs. + +### 2. Extend the data model (`lib/content/projects.ts`) + +Add to the `Project` interface: + +```ts +export interface ProjectImage { + src: string + alt: string +} + +export interface CaseStudySection { + /** Tab label, e.g. "Problem". */ + label: string + /** Paragraphs. Plain text; no markup. */ + paragraphs: string[] +} + +export interface CaseStudy { + headline: string + sections: CaseStudySection[] + facts: { label: string; value: string }[] + links: { label: string; href: string }[] +} + +export interface Project { + // ...existing fields... + /** Card thumbnail. Omit for projects with no UI; the card renders a data tile from `impact`. */ + image?: ProjectImage + /** Short label over the thumbnail, e.g. "Live · sign-in". */ + status?: string + caseStudy?: CaseStudy +} +``` + +Fill in the data. Bracketeer's text comes from Dom's own post — use it nearly verbatim: + +```ts + { + id: '#brkt-0002/05', + name: 'Bracketeer', + // ...existing... + image: { src: '/projects/bracketeer.webp', alt: 'Bracketeer leaderboard mid-tournament' }, + status: 'Live · sign-in', + caseStudy: { + headline: 'Three days before kickoff, with everyone’s picks already made.', + sections: [ + { label: 'Problem', paragraphs: [ + 'The pool started as one HTML file: fill in a bracket, click Export my picks (.csv), email it to the commissioner. Forty-some friends and family did exactly that before the opening match. The plan was to re-enter results after each round and score by hand.', + 'Three days before kickoff I decided that wasn’t good enough. Which set the first constraint before the first commit: every pick already existed, made in a tool I now had to treat as law.', + ]}, + { label: 'What I built', paragraphs: [ + 'A multi-tenant pool platform on Next 16, Prisma 7 and Auth.js, deployed to Railway. Create a pool, invite by link, make picks, watch a leaderboard update from live results. Knockout seeding implements FIFA Annex C.', + 'The first real piece wasn’t a feature. It was a test: the original JavaScript scoring function kept verbatim as an oracle, and the new engine run against it across two thousand randomized brackets.', + ]}, + { label: 'What broke', paragraphs: [ + 'The hard problem wasn’t building fast. It was building fast without ever changing an answer. If the port scored one bracket a single point differently, someone’s standing changed under them.', + 'That oracle test never left the codebase. Every refactor for six weeks had to walk past it.', + ]}, + { label: 'Outcome', paragraphs: [ + 'The pool ran on the app from the round of 32 through the final. Nobody’s score moved during the migration.', + ]}, + ], + facts: [ + { label: 'Status', value: 'Live · account required' }, + { label: 'Players', value: '40+ in one pool' }, + { label: 'Built in', value: '3 days to launch' }, + { label: 'Source', value: 'Private' }, + ], + links: [ + { label: 'Read the build story ↗', href: 'https://dommangonon.substack.com/p/the-game-had-already-started' }, + { label: 'Open live ↗', href: 'https://fifawc26.up.railway.app' }, + ], + }, + }, +``` + +Write SousIQ, Placemat, modular-mind and PRIAL case studies in the same shape from the existing `points` plus what Dom knows. Keep each paragraph under ~60 words; 2–4 sections each. If a fact isn't known, leave it out — never invent numbers. + +### 3. Build `components/landing/CaseStudy.tsx` + +Client component. Props: `{ project: Project; id: string }`. Renders: + +```tsx +'use client' +import { useState } from 'react' +import type { Project } from '@/lib/content/projects' + +export function CaseStudy({ project, id }: { project: Project; id: string }) { + const cs = project.caseStudy! + const [active, setActive] = useState(0) + return ( + + ) +} +``` + +`next/image` is unoptimized in this project (static export), so a plain `` with explicit `width`/`height` is fine; add `// eslint-disable-next-line @next/next/no-img-element` above it. Reuse `.travel-map-toggle` for the tab buttons — it's already the site's ghost-button style. + +### 4. Rework `components/landing/Projects.tsx` + +Make it a client component holding `openId: string | null`. Each card becomes an `
` with a ` +
+ {project.caseStudy && {isOpen ? 'Close ↑' : 'Case study ↓'}} + {project.href && ( + + {project.hrefKind === 'repo' ? 'View source ↗' : 'Open live ↗'} + + )} +
+
+ ) + })} + + {open?.caseStudy && } + + ) +} +``` + +Nesting rule: **no `` inside the ` + {done && Check your inbox to confirm.} + +