Give the Learn cards a real frame instead of a gradient
CI / verify (push) Successful in 3m23s
CI / publish (push) Has been skipped

The preview cards led with a generated gradient. It was a deliberate fallback —
nothing renders a frame of a Cap embed without loading the embed, and loading
nine embeds to decorate a grid is how a page becomes unusable on a phone — but
for videos PIG serves itself the frame is right there in the file.

The poster is named after the VIDEO's content hash, not its own:
`overview.4d4581ae.mp4` -> `overview.4d4581ae.jpg`. Re-rendering a clip changes
both names together, so a thumbnail cannot outlive what it claims to show. It
needs no schema column and no manifest entry, because the name is derivable.

`learnPoster.sh` cuts the frame with `thumbnail=90` starting four seconds in
rather than taking frame 0: the first frame of a Playwright capture is often
mid-paint, and a poster of a half-rendered page is worse than no poster.

The resolver ASSERTS the poster rather than verifying it — @pig/core is pure and
has no filesystem. That is safe in both directions: a missing poster 404s, which
`<video poster>` renders exactly as it renders no poster, and which the card
falls back from via onError. Claiming a poster that is absent is free; omitting
one that exists would cost every card its thumbnail.

Cap-hosted rows are unchanged and still get the gradient, verified by there
being exactly five <img> elements on a page with nine resources.

Also widens the media allowlist to jpg/webp. The filename pattern, the
traversal rules and the symlink check are untouched and still cover them,
because extension is the only axis that changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 17:53:30 -07:00
parent 45b70b17f0
commit 19dd30acbe
5 changed files with 120 additions and 18 deletions
+38 -2
View File
@@ -133,6 +133,10 @@ export const LEARN_MEDIA_CONTENT_TYPES: Record<string, string> = {
mp4: 'video/mp4',
webm: 'video/webm',
m4v: 'video/x-m4v',
// Poster frames. Served from the same directory and the same route as the
// video they were cut from — see `learnPosterFilename`.
jpg: 'image/jpeg',
webp: 'image/webp',
};
/**
@@ -146,7 +150,7 @@ export const LEARN_MEDIA_CONTENT_TYPES: Record<string, string> = {
* unbounded filename is an unbounded path to `stat`.
*/
export const LEARN_MEDIA_FILENAME_PATTERN =
/^(?=.{1,120}$)[A-Za-z0-9][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)*\.(?:mp4|webm|m4v)$/;
/^(?=.{1,120}$)[A-Za-z0-9][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)*\.(?:mp4|webm|m4v|jpg|webp)$/;
export function isLearnMediaFilename(value: string): boolean {
return LEARN_MEDIA_FILENAME_PATTERN.test(value);
@@ -157,6 +161,26 @@ export function learnMediaPath(filename: string): string {
return `${LEARN_MEDIA_PATH_PREFIX}${filename}`;
}
/**
* The poster that belongs to a video file.
*
* Derived by swapping the extension, which keeps the VIDEO's content hash in
* the poster's name: `overview.4d4581ae.mp4` -> `overview.4d4581ae.jpg`.
* Re-rendering a video changes both names together, so a poster can never go
* stale against the clip it claims to show — which a separately hashed or
* hand-named thumbnail would eventually do.
*
* Returns null for a filename that is not a video, so a poster cannot acquire
* a poster of its own.
*/
export function learnPosterFilename(videoFilename: string): string | null {
const dot = videoFilename.lastIndexOf('.');
if (dot <= 0) return null;
const extension = videoFilename.slice(dot + 1).toLowerCase();
if (!LEARN_MEDIA_CONTENT_TYPES[extension]?.startsWith('video/')) return null;
return `${videoFilename.slice(0, dot)}.jpg`;
}
/** The Content-Type for a validated filename, or null if it has no known one. */
export function learnMediaContentType(filename: string): string | null {
const extension = filename.slice(filename.lastIndexOf('.') + 1).toLowerCase();
@@ -343,7 +367,19 @@ export const LEARN_EMBED_REJECTION_MESSAGES: Record<LearnEmbedRejection, string>
/** Build the render instruction for a provider and an already-validated id. */
function embedFor(definition: LearnProviderDefinition, externalId: string): LearnEmbed {
const src = definition.embed(externalId);
return definition.kind === 'video' ? { kind: 'video', src } : { kind: 'iframe', src };
if (definition.kind !== 'video') return { kind: 'iframe', src };
/*
* The poster is asserted, not verified — this module is pure and has no
* filesystem. A poster that was never generated 404s, which a <video poster>
* renders exactly as it renders no poster at all, and which the card falls
* back from. Claiming a missing image is therefore free; omitting one that
* exists would cost every card its thumbnail.
*/
const posterFile = learnPosterFilename(externalId);
return posterFile
? { kind: 'video', src, poster: learnMediaPath(posterFile) }
: { kind: 'video', src };
}
/**