Build Content
Idea → branch → scaffold → build → verify → record → PR with embedded recording. Use the session scratchpad for all intermediate files (raw mp4, gif, frames, batch json).
0. No idea given? Pitch, then ask
If invoked without a component idea, don't ask an open-ended "what do you want?" — come up with strong candidates and let the user pick (AskUserQuestion, with an Other escape hatch).
Judge every candidate against the Content Curation Philosophy in the repo CLAUDE.md — outside-the-web imports, intersection-not-category, state readable from motion alone.
Process: survey what already exists (git ls-files content/ | cut -d/ -f2 | sort -u
plus open PR titles via gh pr list) so pitches don't collide with shipped or
in-flight work; generate 6–8 collisions across DIFFERENT source domains; keep the
3–4 with the strongest motion story; present each as "name — one-line hook" with a
sentence on the interaction. After the pick, confirm the beats you'd record (step 2's
flattering loop) in one sentence and proceed.
1. Branch + scaffold
Preflight: git status --short. The PR must commit apps/web/package.json and
pnpm-lock.yaml — if either already has uncommitted changes, stop and surface it
to the user rather than sweeping their WIP into the PR. Also check
git rev-list --count origin/main..main — unpushed main commits will show up in
the PR diff until main is pushed; mention it in the wrap-up if nonzero.
git checkout -b kyh/<slug> main
pnpm new:content <slug> --description "<one-liner>"
Slug is kebab-case (SLUG_RE in scripts/new-content.ts enforces it). The scaffold
creates content/<slug>/{<slug>.tsx, preview.tsx, meta.json, package.json}, registers
the workspace dep in apps/web/package.json, and runs pnpm install.
2. Build the component
content/<slug>/<slug>.tsx— the component.preview.tsx— stages it fullscreen.- Read 2–3 committed siblings first (
git ls-files content/ | cut -d/ -f2 | sort -u— acontent/<x>/dir holding onlynode_modulesis leftover from another branch, not a real sibling) to absorb conventions:"use client", Tailwind arbitrary classes,motionfor animation (add to the package's ownpackage.jsondeps,pnpm installafter). - Stage on a dark radial-gradient
<main class="flex h-dvh items-center justify-center">like sibling previews — gallery cards and recordings assume it. - The component must demo itself: either it auto-animates, or the preview wires up an auto-tour + visible controls. A recording of a static component is worthless, so design the "flattering loop" now, not at recording time.
meta.json: scaffold already filledname,description,addedAt; addtags, reusing the existing vocabulary. If the idea came from somewhere specific (a tweet, an app, a video), addinspiredBy: [{ label, url }]— provenance is part of the entry, not a nicety.requestedBy: { name, url }only when someone asked for it (see../build-requests/SKILL.md). Tag vocabulary:python3 -c "import json,glob,collections; print(collections.Counter(t for f in glob.glob('content/*/meta.json') for t in json.load(open(f)).get('tags',[])).most_common(30))"
3. Verify locally
pnpm typecheck && pnpm lint
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/preview-frame/<slug>
Dev server not running → start pnpm dev:web in the background.
Turbopack gotcha (bites every new content package): the persistent cache freezes
the Tailwind @source glob, so arbitrary classes that exist only in the NEW package
silently generate no CSS — markup renders unstyled. If the preview looks unstyled:
rm -rf apps/web/.next, restart pnpm dev:web.
Screenshot /preview-frame/<slug> and actually look at it before recording.
4. Record
The staging recipe (viewport locks at record start; occlusion check) lives in
../cover-video/SKILL.md §2–3 — follow that recipe, with these differences:
- Viewport 1280×800 (PR gif, not gallery cover) — so the eval check must print
visible 1280x800, not the1600x900the cover-video recipe shows. - Open and stage
http://localhost:3000/preview-frame/<slug>, thenrecord start <scratch>/<slug>.raw.mp4 --fps 60. Always--fps 60— the GIF downsamples, but a 30fps source aliases drags and springs before the GIF ever sees them. Always.mp4— see the cover-video skill for why webm takes die early. - 10–15s choreography as ONE
agent-browser batchcall: ~3s settle, then the beats you designed in step 2 (drags need intermediatemouse movesteps with 80–120ms waits), then ~3s settle so the loop reads cleanly. - The OS cursor is not captured — favor beats with visible feedback.
After record stop, verify the file: non-zero size AND duration ≥ 8s
(ffprobe -v error -show_entries format=duration -of csv=p=0 <file>). A 0-byte or
sub-second file is a known intermittent agent-browser failure — re-record, waiting
2s after record stop before checking.
5. Convert, frame-check, publish the GIF
.claude/skills/build-content/scripts/publish-recording.sh <slug> <scratch>/<slug>.raw.mp4
Converts to an 800px 12fps GIF, uploads it to the kyh/pr-preview-assets orphan
branch via a temp worktree, verifies the raw URL serves image/gif, and prints the
embed URL. Before trusting it, verify beat coverage, not just motion: list the
beats you designed in step 2, extract a frame at each beat's expected timestamp
(ffmpeg -ss <t> -i <slug>.gif -frames:v 1 f.png), and Read them. Every beat —
especially the climax state (success morph, final reveal, whatever the component
builds to) — must have a frame proving it happened. "Frames differ" is not enough:
a hold released at 95% produces a lively gif that's missing the entire point. If a
beat is absent, fix the choreography (hold/wait durations need ~20% margin over the
component's nominal timings — batch step overhead eats into them) and re-record.
All-identical frames = frozen rAF (occlusion) or missed coords.
Why an orphan branch: GitHub has no API for comment/description file attachments, so
GIFs live on kyh/pr-preview-assets (no shared history with main — zero repo bloat)
and embed via raw.githubusercontent.com. Deleting that branch 404s every PR
recording, so don't prune it.
6. Commit, push, PR
git add content/<slug> apps/web/package.json pnpm-lock.yaml
git commit -m "feat: add <slug> — <tagline>"
git push -u origin kyh/<slug>
PR title matches the commit. Body template:
<2–4 sentences: what the component is, the motion/interaction story, notable
construction details — written like the sibling PRs, dense and concrete.>
## Preview recording

_Recorded locally from [`/preview-frame/<slug>`](https://uicapsule-git-kyh-<slug>-kyh-io.vercel.app/preview-frame/<slug>)._
🤖 Generated with [Claude Code](https://claude.com/claude-code)
(The vercel link is the predictable preview URL — it goes live once the deploy finishes; the gif renders immediately.)
7. Wrap up
- Delete scratch mp4/gif/frames/batch files.
- Report the PR URL and your take on the component.
- Offer, don't do: gallery cover via the
cover-videoskill (separate 1600×900 mp4 pipeline + Blob upload), typically after the PR merges.