Motivation
Two threads converge here:
- Placeholders. SPEC-102 fixtures need stand-in images that are deterministic, offline, and self-contained (for the gallery/harness, the editor, the docs, and AI few-shot). The first attempt — inline
data:image/svg+xml URIs — silently failed: Markdoc's parser (markdown-it's validateLink) only allows data:image/(gif|png|jpeg|webp), not svg+xml (XSS hardening), so the image never parsed. A base64-PNG interim works but bloats fixtures with opaque blobs. icon: sugar. The {% icon name="github" /%} rune exists, but inline icons in prose are clumsy; an  shorthand was wanted.
Both are the same shape: a recognised scheme in an image src, resolved to a generated/looked-up inline SVG.
The pieces are already in place:
- Custom schemes parse.
validateLink only blocks javascript:/data:/vbscript:/file:; placeholder:/icon: pass, so  yields an image node with src intact. - The hook exists. The
image schema transform (packages/runes/src/nodes.ts) already inspects src and, when it matches a known SVG, returns an inline <svg> Tag instead of <img> (the svgFiles branch). A scheme resolver slots in alongside it. - Inline SVG sidesteps the sanitizer. Emitting an
<svg> element (not a data: URL) avoids the data:svg restriction entirely — and is scalable and theme-tintable.
Design
Extend the image schema transform: before the generic <img> fallback, match src against registered schemes. A scheme resolver takes the argument (the part after scheme:) plus the node's alt/title/property and returns a renderable Tag (typically an inline <svg>). Unrecognised schemes fall through to the normal <img> path unchanged.
 → <svg class="rf-placeholder" data-shape="portrait" …>
 → <svg class="rf-icon" aria-label="GitHub" …>
 → <img src="/images/real.png" …> (unchanged)
2. icon:<name>
Resolves <name> against the theme's global icon set — the same source the {% icon %} rune uses — and inlines that SVG, with alt becoming the accessible label. This is the inline shorthand for the icon rune; an unknown icon name warns (dev) and falls back to a neutral glyph or passes through.
3. placeholder:<shape>
Emits a generated inline SVG for a named shape — cover (16:9), square, portrait (3:4), wide, banner, avatar (round), thumbnail. The SVG is a neutral scene (e.g. horizon + sun) sized to the shape's aspect ratio, drawn with theme tokens (var(--rf-color-surface) / --rf-color-muted / --rf-color-border), so placeholders pick up the theme tint and dark mode automatically. Deterministic output (no randomness), so it's screenshot-stable. An unknown shape falls back to cover (with a dev warning).
4. Why inline SVG, not a data-URI
- Sidesteps the sanitizer — an
<svg> element isn't a data: URL, so the validateLink SVG block is irrelevant. - Readable source —
 vs a 600-char base64 blob. - Theme-aware + scalable — references tokens; crisp at any size; adapts to light/dark for free.
- Reusable in real content — authors can draft with
placeholder: and swap in real images later, and reach for icon: inline anywhere.
5. Consumers
The image-consuming runes (figure, gallery, juxtapose, mediatext, showcase) already handle the inline-<svg> output of the existing svgFiles branch, so they accept scheme-resolved SVG the same way. The SPEC-102 fixtures migrate to placeholder:<shape>, superseding the base64-PNG interim — clean, readable, theme-aware fixture images.
6. Registry / extensibility
Schemes are a small registry (scheme → resolver) so the set is explicit and a plugin could, in principle, register its own. Core ships icon: and placeholder:. Keep it minimal — this is sugar, not a general URL-rewriting framework.
Implications
- Output element changes for schemed images (
<svg> not <img>); verified against the runes that wrap media, reusing the existing inline-SVG precedent. - A latent content gotcha is also documented: plain
data:image/svg+xml in author content silently drops (sanitizer). The placeholder:/icon: schemes give a supported path; a docs note should warn about raw data:svg. - Supersedes the PNG-placeholder fixtures — once schemes land, the fixtures swap to
placeholder: and the base64 blobs are removed. - Security: inline SVG from our generators / our icon set is trusted; this does not open arbitrary author SVG (raw data:svg stays blocked).
Acceptance Criteria
- The
image transform resolves registered scheme: srcs to renderables before the <img> fallback; unknown schemes pass through unchanged. icon:<name> inlines the theme icon set's SVG (same source as {% icon %}), with alt as the accessible label; unknown name warns + falls back.placeholder:<shape> emits a deterministic, theme-token-tinted inline SVG for each shape (cover/square/portrait/wide/banner/avatar/thumbnail); unknown shape falls back to cover.figure/gallery/juxtapose/mediatext/showcase render scheme-resolved SVG correctly (light + dark).- SPEC-102 fixtures migrate from base64-PNG to
placeholder:<shape>; the gallery renders them with no network and no leaked literal markdown. - Docs: image-scheme sugar documented (authoring guide), plus a note that raw
data:image/svg+xml is rejected by the parser.
Work breakdown (provisional)
- Scheme registry + resolver hook in the
image transform (nodes.ts); unknown-scheme passthrough. icon: resolver — reuse the icon rune's icon-set resolution; alt/label/a11y.placeholder: resolver — shape→aspect SVG generator, token-tinted, deterministic.- Migrate fixtures to
placeholder:; drop the base64-PNG blobs. - Tests — scheme parse/resolution, each shape, icon lookup + fallback, consumer-rune rendering.
- Docs — authoring guide entry + the raw-data:svg caveat.
References
- Hook:
image schema transform in packages/runes/src/nodes.ts (existing svgFiles inline-SVG branch). - Icon source:
{% icon %} rune (packages/runes/src/index.ts) + theme global icon group. - Fixture consumer / supersedes the interim: SPEC-102 (the base64-PNG placeholders).
- Root cause it resolves: markdown-it
validateLink rejects data:image/svg+xml.