Skip to content

Commit f6b50e7

Browse files
ryan-williamsclaude
andcommitted
fix(og): use data-waitfor on embeds instead of a global wait
Tag the Tweet and BlueSky wrappers with `data-waitfor="iframe"`. The existing `exportSlides` machinery already walks every `[data-waitfor]` element and waits for the inner selector to become visible before screenshotting (PDF / PNG / OG) — so Twitter's and Bluesky's async-mounted widget iframes are now waited on precisely, rather than covered by a blanket fixed delay. Drop the OG `wait` default from 3000 ms → 0. The headmatter override `publish.ogImage.wait` stays as an escape hatch for decks with custom embeds that don't tag a `data-waitfor` signal. Net effect: build is faster (no 3 s per slide), correct (waits exactly as long as the slowest embed needs), and fails loudly if an embed is broken instead of silently producing an empty slot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b143f18 commit f6b50e7

3 files changed

Lines changed: 18 additions & 10 deletions

File tree

packages/client/builtin/BlueSky.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,11 @@ onUnmounted(() => {
278278

279279
<template>
280280
<VDrag v-if="props.draggable !== false" :pos="bskyDragId" :lock-aspect-ratio="bskyAR">
281-
<div ref="wrapper" class="bluesky-fit">
281+
<!-- `data-waitfor="iframe"` signals to `exportSlides` (Playwright build-time
282+
render — PDF/PNG/OG) to wait for Bluesky's widget to inject its iframe
283+
before screenshotting. Otherwise the screenshot can fire before the
284+
widget mounts (BSky's `window.bluesky.scan` populates async). -->
285+
<div ref="wrapper" class="bluesky-fit" data-waitfor="iframe">
282286
<div ref="container" class="slidev-bluesky" :style="innerStyle">
283287
<blockquote
284288
v-if="resolvedUri"
@@ -300,7 +304,7 @@ onUnmounted(() => {
300304
</div>
301305
</VDrag>
302306
<Transform v-else :scale="scale || 1">
303-
<div ref="container" class="slidev-bluesky">
307+
<div ref="container" class="slidev-bluesky" data-waitfor="iframe">
304308
<blockquote
305309
v-if="resolvedUri"
306310
class="bluesky-embed"

packages/client/builtin/Tweet.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@ onUnmounted(() => {
131131

132132
<template>
133133
<VDrag v-if="props.draggable !== false" :pos="`tweet-${id}`" :lock-aspect-ratio="tweetAR">
134-
<div ref="wrapper" class="tweet-fit">
134+
<!-- `data-waitfor="iframe"` signals to `exportSlides` (Playwright build-time
135+
render — PDF/PNG/OG) to wait for Twitter's widget to inject its iframe
136+
before screenshotting. Otherwise the screenshot can fire before the
137+
widget mounts (Twitter's `twttr.widgets.createTweet` resolves async). -->
138+
<div ref="wrapper" class="tweet-fit" data-waitfor="iframe">
135139
<!-- DragControl.associatedLink picks this up via `querySelector('a')`,
136140
surfacing a clickable URL in the control bar when the embed is selected. -->
137141
<a :href="`https://x.com/i/web/status/${id}`" target="_blank" rel="noopener noreferrer" aria-hidden="true" class="tweet-link" />
@@ -146,7 +150,7 @@ onUnmounted(() => {
146150
</div>
147151
</VDrag>
148152
<Transform v-else :scale="scale || 1">
149-
<div ref="tweet" class="tweet slidev-tweet">
153+
<div ref="tweet" class="tweet slidev-tweet" data-waitfor="iframe">
150154
<div v-if="!loaded || tweetNotFound" class="w-30 h-30 my-10px bg-gray-400 bg-opacity-10 rounded-lg flex opacity-50">
151155
<div class="m-auto animate-pulse text-4xl">
152156
<div class="i-carbon:logo-twitter" />

packages/slidev/node/commands/og.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -339,12 +339,12 @@ export async function generateOgShells(
339339
await fs.mkdir(cacheDir, { recursive: true })
340340
const cachePath = (p: SlideOgInfo) => resolve(cacheDir, `${p.no}-${p.contentHash}.${format}`)
341341

342-
// Extra delay (ms) after the page `load` event before screenshotting, so
343-
// dynamic embeds (Twitter, BlueSky, etc. — their iframes mount via script
344-
// post-load) have time to populate. 3 s covers the common case without
345-
// bloating the OG-build wallclock by much. Configurable via headmatter
346-
// `publish.ogImage.wait` (in ms).
347-
const wait: number = typeof ogCfg.wait === 'number' ? ogCfg.wait : 3000
342+
// Extra delay (ms) after the page `load` event before screenshotting. The
343+
// primary mechanism for waiting on async-loading content is `data-waitfor`
344+
// (see `exportSlides` + the Tweet / BlueSky components), so default is 0.
345+
// Escape hatch via headmatter `publish.ogImage.wait` for decks with custom
346+
// embeds that can't (or don't) emit a `data-waitfor` signal.
347+
const wait: number = typeof ogCfg.wait === 'number' ? ogCfg.wait : 0
348348

349349
// Render-key: changes to size/format/wait invalidate the entire cache.
350350
const renderKey = `${size[0]}x${size[1]}.${format}.w${wait}`

0 commit comments

Comments
 (0)