A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Record decision 0023: embed resolution pipeline

+46
+46
docs/decisions/0023-embed-resolution-pipeline.md
··· 1 + # 0023 — Embed resolution pipeline 2 + 3 + - **Status:** Accepted 4 + - **Date:** 2026-06-19 5 + - **Scope:** `src/lib/embeds/*`, `src/lib/blocks/render.ts`, `src/lib/reader/sanitize.ts`, 6 + `src/lib/editor/embed-preview.ts`, the article reading page. 7 + 8 + ## Context 9 + 10 + `core/embed` is allowlisted (Decision 0002) but resolved previews require a WordPress 11 + oEmbed proxy SkyPress doesn't have, so embeds rendered as nothing on reading pages and 12 + pasting an embeddable URL produced no block in the editor. We want atproto post embeds 13 + (an improvement) and working video embeds (a fix). 14 + 15 + ## Decision 16 + 17 + Resolve embeds in SkyPress's own pipeline: 18 + 19 + - A dependency-free `src/lib/embeds/` module (registry → resolve → card) recognises a 20 + small provider set (atproto posts, YouTube, Vimeo; one entry to add more) and builds 21 + trusted card HTML with every PDS/provider value escaped. 22 + - Reading pages run an async `resolveEmbeds` pre-pass (it can't live in the synchronous, 23 + dependency-free `render-article` pipeline) that attaches resolved DATA onto each 24 + `core/embed` node; `render.ts` renders the card synchronously; `sanitizeArticleHtml` 25 + runs last (Decision 0018). Server fetches go through `safeFetch` (rule 6a). 26 + - Video is a **facade**: the sanitised HTML carries no iframe — only a thumbnail, title, 27 + and a play button with `data-embed-provider`/`data-embed-id`. Reader JS reconstructs the 28 + playback URL from provider + id and re-validates it against a two-host allowlist 29 + (`youtube-nocookie.com`, `player.vimeo.com`) before inserting the iframe. So the widened 30 + sanitiser (which allows `button` + the two scoped data attributes, never `iframe`) 31 + cannot be abused by a hostile PDS to load an arbitrary frame. 32 + - The editor reuses the same fetch + card code through a `@wordpress/api-fetch` middleware 33 + over `core/embed`: live cards for atproto (CORS-friendly AppView), placeholders for 34 + video (oEmbed isn't browser-CORS-reachable). Pasting a URL still auto-creates the block. 35 + 36 + ## Consequences 37 + 38 + - View links in atproto cards use `atmospherePostWebUrl` → mu.social (Decision 0022). 39 + - SSR resolves embeds per request; v1 relies on `safeFetch` timeouts + graceful link 40 + fallback. Caching (Cloudflare Cache API / in-worker memo) is a fast-follow if latency 41 + warrants it. 42 + - RSS renders embeds as plain links in v1 (the feed builder stays pure/test-locked); 43 + atproto-card-in-RSS is a documented fast-follow. 44 + - Adding a provider = one registry entry + a resolve branch + a card branch + tests. 45 + - The embed card intentionally diverges from `serialize()` (read-time enhancement, like 46 + blob-image resolution) — the fidelity test (rule 4) does not cover `core/embed`.