Skip to main content

max / makenotwork

Shoot the carousel frames a visitor would recognise Three things the first real run got wrong, none of which the earlier no-credential trial could have shown. Log in lazily, right before the frame that needs it. Signing in up front changed the two anonymous frames, which is the point of their being anonymous: a storefront links a stranger to /purchase/<id> and the demo buyer, who owns nine of the catalogue, to /i/<id>. Frame 2 was shooting an owned-item page under the caption "every sale is yours". Shoot from a storefront that sells. restored-reels-vol-1 is the seed's free / name-your-price project, so every item priced at Free and slide 2 argued for 0% platform fee over a page with nothing for sale. deskriver-suite sells at $8. Rewrite /purchase/<id> to /i/<id>. What a logged-out storefront links is a two-step checkout titled "Confirm Purchase", not the item page the caption describes. The item page is public, and it carries the price, the buy button and the download details the alt text promises. Also wait for images and the card reveal before shooting, both on a deadline so an endless HTMX spinner cannot hang the run, and name the page a frame landed on when its selector is missing.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-07 23:23 UTC
Signed with PGP, not checked
Commit: 77ff2131a58a31e3017caac4926dc5ea25b5ea93
Parent: 9a1e1e3
1 file changed, +86 insertions, -25 deletions
@@ -87,10 +87,20 @@
87 87 * The three frames, in carousel order. Paths are resolved against BASE.
88 88 * `alt` mirrors what landing.rs claims the frame shows; keep them in step.
89 89 */
90 + // The storefront both public frames are shot from.
91 + //
92 + // It has to be a project that actually SELLS. The captions are "sell anything
93 + // digital" and "every sale is yours. 0% platform fee", and the seeded
94 + // `restored-reels-vol-1` is the free / name-your-price project by design: all
95 + // five of its items price at Free, so both slides argued for the fee on work
96 + // that carries no fee. `deskriver-suite` sells at $8. Override with
97 + // CAPTURE_STOREFRONT.
98 + const STOREFRONT = process.env.CAPTURE_STOREFRONT ?? '/p/deskriver-suite';
99 +
90 100 const FRAMES = [
91 101 {
92 102 name: 'storefront',
93 - path: '/p/restored-reels-vol-1',
103 + path: STOREFRONT,
94 104 alt: "A creator's storefront showing their listed items with prices and cover art",
95 105 needsAuth: false,
96 106 },
@@ -100,27 +110,29 @@
100 110 // resolves one from the storefront at capture time rather than pinning an
101 111 // id that dies on the next seed.
102 112 //
103 - // Two shapes are possible and which one appears depends on the visitor.
104 - // `/i/<id>` is the item page proper; `/purchase/<id>` is what a storefront
105 - // links to for an item the visitor cannot access, which on a no-login demo
106 - // is every paid item. Prefer the item page, take the purchase page when
107 - // that is all there is: it carries the price and the buy button the frame's
108 - // alt text promises. Override with CAPTURE_ITEM_PATH.
113 + // A storefront links a logged-out visitor to `/purchase/<id>`, not to the
114 + // item page. That is a two-step checkout interstitial titled "Confirm
115 + // Purchase", not the page this frame's caption describes, so rewrite it to
116 + // `/i/<id>`: the item page is public (200 to anonymous) and carries the
117 + // price, the buy button and the download details the alt text promises.
118 + // Override the whole thing with CAPTURE_ITEM_PATH.
109 119 resolvePath: async (send) => {
110 120 if (process.env.CAPTURE_ITEM_PATH) return process.env.CAPTURE_ITEM_PATH;
111 121 const { result } = await send('Runtime.evaluate', {
112 122 expression: `(() => {
113 - const pick = (sel) => {
114 - const a = document.querySelector(sel);
115 - return a ? new URL(a.href).pathname : '';
116 - };
117 - return pick('a[href^="/i/"]') || pick('a[href^="/purchase/"]');
123 + // Prefer an item the storefront prices, so the frame shows a sale.
124 + const priced = [...document.querySelectorAll('a[href^="/i/"], a[href^="/purchase/"]')]
125 + .find((a) => /\\$\\s*\\d/.test(a.closest('article, li, .item-card, div')?.textContent ?? ''));
126 + const a = priced ?? document.querySelector('a[href^="/i/"], a[href^="/purchase/"]');
127 + if (!a) return '';
128 + // /purchase/<id> and /i/<id> share the id; the item page is the frame.
129 + return new URL(a.href).pathname.replace(/^\\/purchase\\//, '/i/');
118 130 })()`,
119 131 returnByValue: true,
120 132 });
121 133 return result.value;
122 134 },
123 - from: '/p/restored-reels-vol-1',
135 + from: STOREFRONT,
124 136 alt: 'An item page with its price, buy button, and download details',
125 137 needsAuth: false,
126 138 },
@@ -330,23 +342,43 @@
330 342 });
331 343 }
332 344
333 - if (BUYER_PASSWORD) {
334 - const failure = await logIn(call, () => once('Page.loadEventFired', sessionId));
335 - if (failure) {
336 - console.error(` login: ${failure}`);
337 - console.error(' the library frame will be skipped');
345 + // Log in LAZILY, immediately before the first frame that needs a session.
346 + //
347 + // Signing in up front changes the two anonymous frames, which is the whole
348 + // reason they are anonymous. The storefront links an item the visitor can
349 + // reach: to a stranger that is `/purchase/<id>`, carrying the price and the
350 + // buy button frame 2's caption promises, and to the demo buyer, who owns
351 + // nine of the catalogue, it is `/i/<id>`, the owned-item page with a
352 + // download button instead. Frame 2 would have quietly advertised "every
353 + // sale is yours" over a page with nothing for sale.
354 + let signedIn = Boolean(SESSION_COOKIE);
355 + let loginFailure = null;
356 + const ensureSignedIn = async () => {
357 + if (signedIn || loginFailure || !BUYER_PASSWORD) return;
358 + loginFailure = await logIn(call, () => once('Page.loadEventFired', sessionId));
359 + if (loginFailure) {
360 + console.error(` login: ${loginFailure}`);
338 361 } else {
362 + signedIn = true;
339 363 console.error(` login: signed in as ${BUYER_LOGIN}`);
340 364 }
341 - }
365 + };
342 366
343 367 for (const frame of FRAMES) {
344 - if (frame.needsAuth && !CAN_AUTH) {
345 - console.error(
346 - ` ${frame.name}: skipped, needs CAPTURE_BUYER_PASSWORD (or CAPTURE_SESSION_COOKIE)`,
347 - );
348 - missing.push(frame.name);
349 - continue;
368 + if (frame.needsAuth) {
369 + if (!CAN_AUTH) {
370 + console.error(
371 + ` ${frame.name}: skipped, needs CAPTURE_BUYER_PASSWORD (or CAPTURE_SESSION_COOKIE)`,
372 + );
373 + missing.push(frame.name);
374 + continue;
375 + }
376 + await ensureSignedIn();
377 + if (!signedIn) {
378 + console.error(` ${frame.name}: skipped, not signed in`);
379 + missing.push(frame.name);
380 + continue;
381 + }
350 382 }
351 383
352 384 // Resolve the URL, navigating to a source page first when the frame's
@@ -370,6 +402,35 @@
370 402 expression: 'document.fonts.ready.then(() => true)',
371 403 awaitPromise: true,
372 404 });
405 +
406 + // Wait for every image to finish, and for the reveal animation the cards
407 + // ride in on to land. `load` fires before lazy covers arrive, and the item
408 + // grid fades in, so the first storefront capture caught the cards at part
409 + // opacity: three grey rectangles reading "Sample", "Audio", "Video" and no
410 + // artwork at all, on the one slide whose whole job is showing the goods.
411 + //
412 + // Scroll the grid into view first: a lazy image below the fold never
413 + // starts loading, and the crop is taller than the viewport's first screen.
414 + await call('Runtime.evaluate', {
415 + expression: `(async () => {
416 + // Everything here races a deadline. An HTMX indicator spins forever,
417 + // so its animation's finished promise never settles, and a lazy image
418 + // that 404s can leave a pending decode: without the cap the run hangs
419 + // instead of shooting a slightly early frame.
420 + const capped = (p, ms) => Promise.race([p, new Promise((r) => setTimeout(r, ms))]);
421 + window.scrollTo(0, document.body.scrollHeight);
422 + await new Promise((r) => setTimeout(r, 250));
423 + window.scrollTo(0, 0);
424 + await capped(Promise.all([...document.images].map((i) => i.complete
425 + ? Promise.resolve()
426 + : new Promise((r) => { i.addEventListener('load', r, {once: true});
427 + i.addEventListener('error', r, {once: true}); }))), 8000);
428 + await capped(Promise.all(
429 + document.getAnimations().map((a) => a.finished.catch(() => {}))), 2000);
430 + return true;
431 + })()`,
432 + awaitPromise: true,
433 + });
373 434 await sleep(SETTLE_MS);
374 435
375 436 const selector = frame.selector ?? DEFAULT_SELECTOR;