mcpbeat

Onboarding Videos

posthog/onboarding-videos

Add, replace, and optimize the looping demo videos in the onboarding "welcome" bento grid (packages/ui/src/features/onboarding). Covers the ffmpeg compression workflow (the scripts/optimize-onboarding-videos.mjs wrapper), the faststart + right-size best practices that keep playback smooth, the first-frame poster convention, and how to wire a new clip into WelcomeScreen. Use when onboarding demo clips feel laggy, when a clip is being added/swapped/re-recorded, or when posters and videos drift out of sync.

This is a copy. The original lives at posthog/code-onboarding-videos.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
690
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/PostHog/posthog --skill onboarding-videos

What it tells the agent to use

found in the instruction text
Bash runs shell commands — read the instruction before connecting

The instruction itself

8 sections, as written by the author

Onboarding bento demo videos

The first onboarding step (WelcomeScreen) shows a bento grid of feature cards.

A card with a media entry plays a short, muted, looping screen recording; cards

without one show a static placeholder. The featured (large, top-left) card is the

only one that plays at a time — hover just moves the highlight.

| Thing | Where |

| --- | --- |

| Clips + posters | packages/ui/src/features/onboarding/assets/<feature>-<light\|dark>.{mp4,jpg} |

| Wiring (MEDIA map, startTime) | packages/ui/src/features/onboarding/components/WelcomeScreen.tsx |

| <video> element + play/seek logic | packages/ui/src/features/onboarding/components/FeatureBentoCard.tsx |

| Optimizer script | scripts/optimize-onboarding-videos.mjs (pnpm optimize:onboarding-videos) |

Why clips need optimizing (the "it feels laggy" fix)

Raw screen recordings hitch in this UI for two reasons, both fixed by re-encoding:

  • moov atom at the end of the file. Without faststart the player must read

to EOF before it can start, so first-play and every seek stutter. We seek on

mount and on every loop (FeatureBentoCard parks the clip on videoStartTime),

so this bites constantly. Fix: -movflags +faststart.

  • Resolution far larger than it renders. The featured slot is never wider than

~500 CSS px (the grid is max-w-[760px]). Even at 2× retina that's ~1000px, but

recordings come in at ~1876px. Decoding huge frames and downscaling them — on

every play and during the framer-motion slot reflow — is wasted work. Fix: cap

width at 1000px (which also matches the poster width exactly).

Bitrate/size are usually already modest; decode cost and startup are the lag,

not bytes. Don't chase file size at the expense of width/faststart.

Canonical encode target

H.264 · yuv420p · ≤1000px wide (keep aspect, even height) · CRF 23 ·

+faststart · no audio. These live as constants at the top of

scripts/optimize-onboarding-videos.mjs — change them there, not ad-hoc.

Optimize existing clips

pnpm optimize:onboarding-videos          # encode any clip not already optimized
pnpm optimize:onboarding-videos --dry-run   # show what would change
pnpm optimize:onboarding-videos --force     # re-encode all (after changing the target constants)

Requires ffmpeg (brew install ffmpeg). The script tags each output with a

comment marker, so re-runs skip already-optimized files — it's safe to run any

time, including right after dropping in a new clip. It rewrites files in place;

review the git diff --stat and the printed before/after sizes.

Add or replace a clip

  • Record light + dark variants at ≥1000px wide. The optimizer downscales to

1000px but never upscales, so anything narrower ships soft — resolution is the one

thing it can't fix for you. Keep it short (~10–13s); it loops.

  • Name + drop the files as assets/<feature>-light.mp4 and

assets/<feature>-dark.mp4. Two things bite every time:

  • Recordings almost always arrive with the light variant unsuffixed

(foo.mp4, only foo-dark.mp4 is tagged). Rename it to foo-light.mp4.

  • The source filename (e.g. whatever's in ~/Downloads) is irrelevant — the

<feature> prefix must be the media id you'll use in WelcomeScreen and

follow the existing convention, not whatever the file was called.

No assets.d.ts change needed — *.mp4/*.jpg are wildcard modules.

  • Optimize: pnpm optimize:onboarding-videos.
  • Make the poster — the still shown before play. Use the clip's first frame

and leave startTime at 0, so the poster, the first played frame, and the loop

point are all the same with nothing to keep in sync:

   ffmpeg -y -i assets/<feature>-<theme>.mp4 \
     -frames:v 1 -vf "scale=1000:-2:flags=lanczos" -q:v 3 \
     assets/<feature>-<theme>.jpg
  • Wire it up in WelcomeScreen.tsx. For a *new* media id, four edits, all keyed

by the same slug: import the .mp4 + .jpg, add the id to the MediaId union,

add its entry to the MEDIA map (startTime: 0), and set media: "<id>" on the

target FeatureDef.

Replacing an existing clip

Keep the same asset filename and the wiring is untouched — none of step 5

applies. Drop the new file over assets/<feature>-<theme>.mp4, run the optimizer (a

fresh drop carries no skip-marker, so it re-encodes without --force), and

regenerate that poster (step 4). If you replace only one theme, re-check that

light and dark still share an aspect ratio — a clip with stray padding frames

differently from its sibling, and the gap shows when the user toggles theme.

Poster = first frame (and startTime = 0)

FeatureBentoCard shows the poster while a card rests, then seeks the <video> to

MEDIA[id].startTime on mount and loops back there (not necessarily to 0). The

simple, default contract: **the poster is the clip's first frame and startTime is

0**, so the still, the first played frame, and the loop point are all identical —

nothing to keep in sync. Keep the poster the same pixel width as the clip (1000px)

so the poster→video swap is seamless.

startTime can start/loop mid-clip if you ever need it (the code-review clip uses

3s) — but then the poster MUST be that exact frame (ffmpeg -ss <startTime> -i …),

or the still jumps the instant playback starts. Prefer the first-frame default

unless you have a specific reason.

Verify

# clips: moov should print BEFORE mdat, width ≤ 1000:
ffprobe -v trace <clip>.mp4 2>&1 | grep -o -m2 -E "type:'(moov|mdat)'"
ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 <clip>.mp4

# wiring: a grown MediaId union may need a formatter reflow, so let biome fix it:
npx biome check --write packages/ui/src/features/onboarding/components/WelcomeScreen.tsx
pnpm --filter @posthog/ui typecheck

To confirm playback feels smooth in the real app, use the test-electron-app

skill to drive the running onboarding flow.

How to use it

Copy the folder

Take posthog/onboarding-videos from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference npx, brew. Without those the skill loads but fails at the first command.