···11+# 0023 — Embed resolution pipeline
22+33+- **Status:** Accepted
44+- **Date:** 2026-06-19
55+- **Scope:** `src/lib/embeds/*`, `src/lib/blocks/render.ts`, `src/lib/reader/sanitize.ts`,
66+ `src/lib/editor/embed-preview.ts`, the article reading page.
77+88+## Context
99+1010+`core/embed` is allowlisted (Decision 0002) but resolved previews require a WordPress
1111+oEmbed proxy SkyPress doesn't have, so embeds rendered as nothing on reading pages and
1212+pasting an embeddable URL produced no block in the editor. We want atproto post embeds
1313+(an improvement) and working video embeds (a fix).
1414+1515+## Decision
1616+1717+Resolve embeds in SkyPress's own pipeline:
1818+1919+- A dependency-free `src/lib/embeds/` module (registry → resolve → card) recognises a
2020+ small provider set (atproto posts, YouTube, Vimeo; one entry to add more) and builds
2121+ trusted card HTML with every PDS/provider value escaped.
2222+- Reading pages run an async `resolveEmbeds` pre-pass (it can't live in the synchronous,
2323+ dependency-free `render-article` pipeline) that attaches resolved DATA onto each
2424+ `core/embed` node; `render.ts` renders the card synchronously; `sanitizeArticleHtml`
2525+ runs last (Decision 0018). Server fetches go through `safeFetch` (rule 6a).
2626+- Video is a **facade**: the sanitised HTML carries no iframe — only a thumbnail, title,
2727+ and a play button with `data-embed-provider`/`data-embed-id`. Reader JS reconstructs the
2828+ playback URL from provider + id and re-validates it against a two-host allowlist
2929+ (`youtube-nocookie.com`, `player.vimeo.com`) before inserting the iframe. So the widened
3030+ sanitiser (which allows `button` + the two scoped data attributes, never `iframe`)
3131+ cannot be abused by a hostile PDS to load an arbitrary frame.
3232+- The editor reuses the same fetch + card code through a `@wordpress/api-fetch` middleware
3333+ over `core/embed`: live cards for atproto (CORS-friendly AppView), placeholders for
3434+ video (oEmbed isn't browser-CORS-reachable). Pasting a URL still auto-creates the block.
3535+3636+## Consequences
3737+3838+- View links in atproto cards use `atmospherePostWebUrl` → mu.social (Decision 0022).
3939+- SSR resolves embeds per request; v1 relies on `safeFetch` timeouts + graceful link
4040+ fallback. Caching (Cloudflare Cache API / in-worker memo) is a fast-follow if latency
4141+ warrants it.
4242+- RSS renders embeds as plain links in v1 (the feed builder stays pure/test-locked);
4343+ atproto-card-in-RSS is a documented fast-follow.
4444+- Adding a provider = one registry entry + a resolve branch + a card branch + tests.
4545+- The embed card intentionally diverges from `serialize()` (read-time enhancement, like
4646+ blob-image resolution) — the fidelity test (rule 4) does not cover `core/embed`.