max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
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; |