Skip to content

feat: render loading placeholder for incomplete streaming images - #504

Merged
farnabaz merged 5 commits into
vercel:mainfrom
sleitor:feat/incomplete-image-placeholder
Sep 1, 2026
Merged

feat: render loading placeholder for incomplete streaming images#504
farnabaz merged 5 commits into
vercel:mainfrom
sleitor:feat/incomplete-image-placeholder

Conversation

@sleitor

@sleitor sleitor commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #503 — incomplete images during streaming now render an animated skeleton placeholder instead of being silently removed. This mirrors the existing behavior for incomplete links.

What changed


  • link-image-handler.ts: Both handleIncompleteUrl() and handleIncompleteText() now emit ![alt](streamdown:incomplete-image) for incomplete images instead of stripping them entirely
  • index.ts: Updated earlyReturn check to also short-circuit on streamdown:incomplete-image; fixed JSDoc for images option
  • Tests updated: removal expectations → placeholder expectations

packages/streamdown

  • lib/image.tsx: ImageComponent detects src === 'streamdown:incomplete-image' and renders an animate-pulse skeleton div with data-incomplete="true"
  • Tests added for the new placeholder behavior

Changeset: remend@patch + streamdown@patch

Before / After

Before: ![logo](./assets/log → (image silently removed, layout shift)

After: ![logo](./assets/log → animated grey skeleton placeholder shown until the stream completes

Test coverage

  • 356 remend tests ✅
  • 964 streamdown tests ✅

All existing tests updated; 4 new tests added for the placeholder behavior.

@vercel

vercel Bot commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

@sleitor is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

@farnabaz farnabaz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @sleitor
Do you mind removing line numbers commit from this PR?

Also I do think streamdown schema will be blocked by rehype-harden package we need to change placeholder to another text. maybe even an 1x1 pixel placeholder

data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>
// or equivalent png

Incomplete images during streaming (e.g. `![alt](https://exampl`) now
render an animated skeleton placeholder instead of being silently removed.

Changes:
- remend: emit `![alt](streamdown:incomplete-image)` for incomplete images
  instead of stripping them entirely, in both handleIncompleteUrl() and
  handleIncompleteText()
- remend: add earlyReturn check for `streamdown:incomplete-image` marker
- streamdown: ImageComponent renders animate-pulse skeleton div when
  src === 'streamdown:incomplete-image'
- Tests updated to reflect new placeholder behavior

Mirrors the existing incomplete link handling (streamdown:incomplete-link).

Closes vercel#503
Replace the streamdown:incomplete-image custom-scheme marker with a
1x1 transparent PNG data: URI. Non-http(s)/data URL schemes can be
stripped by strict URL-sanitizing rehype/remark plugins (e.g.
rehype-harden), which would prevent the placeholder from ever reaching
the ImageComponent's src check.

Addresses review feedback from @farnabaz on vercel#504.
@sleitor
sleitor force-pushed the feat/incomplete-image-placeholder branch from 44ea6a3 to 4a07096 Compare August 30, 2026 17:10
@sleitor

sleitor commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review, @farnabaz!

  • Removed the unrelated lineNumbers commit that got mixed into this branch (rebased, now only the placeholder feature + this fix remain).
  • Replaced the streamdown:incomplete-image custom-scheme marker with a 1x1 transparent PNG data: URI (INCOMPLETE_IMAGE_PLACEHOLDER, exported from remend). This should survive strict URL-sanitizing schemas like rehype-harden, since only http(s)/data schemes are typically allowlisted — a bare custom scheme string would likely get stripped before it reaches the ImageComponent's src check.

All 356 remend + 951 streamdown tests pass, lint is clean. Let me know if you'd like a different data URI (e.g. transparent GIF/SVG) or any other adjustments.

# Conflicts:
#	packages/streamdown/__tests__/image.test.tsx
Replace the shared 1x1 PNG data URI marker with streamdown:incomplete-image
(link parity). Allowlist the streamdown protocol on img src in the default
sanitize schema so the sentinel survives rehype-sanitize + rehype-harden,
and add coverage that mirrors incomplete-link sanitize tests.
@farnabaz

farnabaz commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Thanks @sleitor,
The more I think, the more get to this idea that your initial work on src was right, having empty 1x1 image in the src make no sense. streamdown:incomplete-image makes it smaller and easier to detect.
I just updated you PR to put back streamdown:incomplete-image and add streamdown protocol in src accepted protocols.

@farnabaz
farnabaz merged commit e0ce123 into vercel:main Sep 1, 2026
5 of 6 checks passed
@sleitor
sleitor deleted the feat/incomplete-image-placeholder branch September 2, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming support for partial images (similar to incomplete link handling)

2 participants