Summary
The graceful-render branch for incomplete streaming links in MarkdownA (data-incomplete={isIncomplete} keyed on href === "streamdown:incomplete-link") is structurally dead code: the sentinel URL that remend injects can never reach the component, because Streamdown's own default rehype pipeline strips it first. The observable result is that the DEFAULT remend.linkMode: "protocol" flashes {label} [blocked] on every incomplete link mid-stream — the exact symptom of #156 — and the intended data-incomplete styling path never fires.
This is distinct from #156 / #165 / #301 (all closed): those report the flash or ask for an escape hatch (resolved by the opt-in remend.linkMode: "text-only"). This report is that the default mode's own healing mechanism is internally unreachable, so the default is structurally broken rather than merely opinionated.
Mechanism (all refs on main, reproduces on 2.5.0)
- remend (string level, streaming mode) completes a partial
[label](ur… or see [the docs tail into [label](streamdown:incomplete-link) — linkMode: "protocol" default.
- rehype-sanitize runs with the built-in schema (
packages/streamdown/index.tsx ~L239-247), which extends defaultSchema.protocols.href with only "tel". defaultSchema.protocols.href is ['http','https','irc','ircs','mailto','xmpp'] — streamdown: is not in the list, so the sentinel href is deleted.
- rehype-harden (pipeline order
raw → sanitize → harden, index.tsx ~L251-257) then sees an href-less <a>; transformUrl(undefined, …) returns null → the default indicator block policy replaces the anchor with <span title="Blocked URL: undefined" …>{label} [blocked]</span>. (Harden's own allowedProtocols: ["*"] would have allowed the sentinel — the block trigger is the sanitize-deleted href, not the protocol.)
- By render time the node is a
<span>, so components.a / MarkdownA is never invoked for it — the isIncomplete branch (packages/streamdown/lib/components.tsx ~L273, data-incomplete at ~L322/L344) cannot execute for streamed-incomplete links.
There is also no user-side escape at the schema layer: allowedTags merges tagNames/attributes only (no protocols extension), and replacing rehypePlugins wholesale disables the allowedTags mechanism.
Suggested fix (either restores the designed behavior)
- Add the sentinel protocol to the built-in sanitize schema:
href: [...(defaultSchema.protocols?.href ?? []), "tel", "streamdown"] — one line; harden already allowlists * protocols, and MarkdownA renders the sentinel case with data-incomplete + no navigable href, so nothing user-navigable leaks; or
- flip the default
linkMode to "text-only" so the default never emits a sentinel it immediately destroys.
Repro
import { Streamdown } from "streamdown"; // 2.5.0
renderToStaticMarkup(<Streamdown mode="streaming">{"see [the docs"}</Streamdown>);
// → …see <span title="Blocked URL: undefined" class="text-gray-500">the docs [blocked]</span>
// expected (per MarkdownA's isIncomplete branch): an <a data-incomplete="true"> styled as pending
Workaround we ship in production: remend={{ linkMode: "text-only" }} — works as documented; this issue is only about the default mode's unreachable branch.
Summary
The graceful-render branch for incomplete streaming links in
MarkdownA(data-incomplete={isIncomplete}keyed onhref === "streamdown:incomplete-link") is structurally dead code: the sentinel URL that remend injects can never reach the component, because Streamdown's own default rehype pipeline strips it first. The observable result is that the DEFAULTremend.linkMode: "protocol"flashes{label} [blocked]on every incomplete link mid-stream — the exact symptom of #156 — and the intendeddata-incompletestyling path never fires.This is distinct from #156 / #165 / #301 (all closed): those report the flash or ask for an escape hatch (resolved by the opt-in
remend.linkMode: "text-only"). This report is that the default mode's own healing mechanism is internally unreachable, so the default is structurally broken rather than merely opinionated.Mechanism (all refs on
main, reproduces on 2.5.0)[label](ur…orsee [the docstail into[label](streamdown:incomplete-link)—linkMode: "protocol"default.packages/streamdown/index.tsx~L239-247), which extendsdefaultSchema.protocols.hrefwith only"tel".defaultSchema.protocols.hrefis['http','https','irc','ircs','mailto','xmpp']—streamdown:is not in the list, so the sentinelhrefis deleted.raw → sanitize → harden,index.tsx~L251-257) then sees an href-less<a>;transformUrl(undefined, …)returnsnull→ the defaultindicatorblock policy replaces the anchor with<span title="Blocked URL: undefined" …>{label} [blocked]</span>. (Harden's ownallowedProtocols: ["*"]would have allowed the sentinel — the block trigger is the sanitize-deleted href, not the protocol.)<span>, socomponents.a/MarkdownAis never invoked for it — theisIncompletebranch (packages/streamdown/lib/components.tsx~L273,data-incompleteat ~L322/L344) cannot execute for streamed-incomplete links.There is also no user-side escape at the schema layer:
allowedTagsmergestagNames/attributesonly (noprotocolsextension), and replacingrehypePluginswholesale disables theallowedTagsmechanism.Suggested fix (either restores the designed behavior)
href: [...(defaultSchema.protocols?.href ?? []), "tel", "streamdown"]— one line; harden already allowlists*protocols, andMarkdownArenders the sentinel case withdata-incomplete+ no navigable href, so nothing user-navigable leaks; orlinkModeto"text-only"so the default never emits a sentinel it immediately destroys.Repro
Workaround we ship in production:
remend={{ linkMode: "text-only" }}— works as documented; this issue is only about the default mode's unreachable branch.