From 931807f3a5bf146230ae5ff8b00d4cd184612aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 30 Aug 2026 00:17:53 +0100 Subject: [PATCH] video posters get a ladder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A poster was one file at the video's full size — 1280x720 for a hero displayed 412 CSS pixels wide. Three times the pixels a phone can show, on the element that IS the page's Largest Contentful Paint, which is the most expensive place on the site to be careless. 384, 512, 768, 1024, plus poster.webp as the widest rung. Those are viewport widths times the density behind them: 412 at 1.75 wants 768, a tablet wants 768, a desktop wants 1280. Cut from the still rather than from poster.webp, so each rung is scaled once from the original frame instead of being a re-encode of an encode. poster.webp keeps its name — the placeholder is cut from it, the site falls back to it, and a directory built before this has only that one. Co-Authored-By: Claude Opus 5 (1M context) --- api/derive.ts | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/api/derive.ts b/api/derive.ts index a257089..776b4b0 100644 --- a/api/derive.ts +++ b/api/derive.ts @@ -109,6 +109,26 @@ export const LADDER = [320, 480, 640, 960, 1280, 1920, 2560] as const; */ export const SQUARE_LADDER = [256, 384, 480, 640, 960] as const; +/** + * Poster rungs, in pixels of width — `poster-768.webp`, and so on. + * + * A video's poster was one file at the source's full size: 1280x720 for a hero displayed 412 CSS + * pixels wide. That is three times the pixels a phone can show, on the element that IS the page's + * Largest Contentful Paint — the most expensive place on the site to be careless. + * + * A hero is full-bleed, so the widths are viewport widths times the density behind them: + * + * 412 x 1.75 = 721 -> 768 (the emulated phone, and most of the real ones) + * 412 x 3 = 1236 -> 1280 (a dense phone, from the full-size poster) + * 768 x 1 = 768 -> 768 (a tablet) + * 1280 x 1 = 1280 -> 1280 (a desktop) + * + * `poster.webp` keeps its name and stays the widest rung. Everything already refers to it — the + * placeholder is cut from it, the site falls back to it, and a directory built before this has only + * that one. + */ +export const POSTER_LADDER = [384, 512, 768, 1024] as const; + /** * WebP quality, by rung. * @@ -639,7 +659,32 @@ async function buildVideo( */ const placeholder = await encodePlaceholder(stillJpg, join(staging, 'placeholder.webp'), email); await rm(stillJpg, { force: true }); - variants.push({ file: 'poster.webp', width: null, height: null, bytes: poster.bytes }); + variants.push({ file: 'poster.webp', width: width || null, height: height || null, bytes: poster.bytes }); + + /* + The poster's own ladder, cut from the same still. + + From `stillJpg` rather than from `poster.webp`, so each rung is scaled once from the original frame + instead of being a re-encode of an encode. The still is a JPEG at the video's full size and it is + about to be deleted anyway. + + Rungs narrower than the source only: one at or above it would be `poster.webp` under another name. + */ + for (const rung of POSTER_LADDER) { + if (!width || rung >= width) continue; + const cut = await post<{ path: string; bytes: number }>('/v1/sharp', email, { + inputPath: stillJpg, + outputPath: join(staging, `poster-${rung}.webp`), + pipeline: [{ resize: { width: rung, fit: 'inside', withoutEnlargement: true } }], + output: { format: 'webp', options: { quality: qualityFor(rung) } }, + }); + variants.push({ + file: `poster-${rung}.webp`, + width: rung, + height: height ? Math.round((rung / width) * height) : null, + bytes: cut.bytes, + }); + } return { kind: 'video',