Lossless Changelog
fleet

v1 ship: YouTube playlist embed family functional

Started the day fixing how a single-line YouTube Short renders inside a memo's prose flow. Ended it with a build-time-fetched playlist UI that escape-positions itself in the article's right margin, with a 1,899-item scroller wired to the iframe via the YouTube IFrame Player API. Six commits, three changelogs, two specs touched, one new cross-cutting `MobileFeaturePlaceholder` pattern.

Michael StatonMusic Editoraugmented with Claude Code on Claude Opus 4.7

Changelog - 2026-05-07 (#3)

v1 ship: YouTube playlist embed family functional

Why Care?

The Lossless Flavored Markdown video-component family is the first part of LFM that crossed from "auto-unfurl ships you a working iframe" to "auto-unfurl ships you a thoughtful, theme-aware, design-system-aligned UI surface." The playlist case was the proving ground because it's the hardest one — a single-video embed has one rendering decision; a playlist has a dozen (item list, sort, click-to-play, lazy-load, mobile fallback, vertical alignment, scroll affordances, ...). All of those are now wired and working in v1.

This entry exists mostly to capture the full day's session arc as a single referenceable table — the work split across two changelog entries (_01 for the first-pass playlist landing, _02 for the cross-cutting aside-escape pattern) plus this third one for the ship close-out. Six commits in the site repo, four in the parent.

Today's session arc, in commits

Site repo (sites/mpstaton-site on development branch)

CommitMessageWhat
36e3d50chore(lfm-dev, content-schema): switch lfm:local to relative link path, relax frontmatter schemaMade local lfm dev work without forcing the site into the astro-knots workspace. Relaxed augmented_with schema to accept lists.
143ea6bfeat(shorts, lfm-directives): float Shorts as aside, wire :::youtube-short with versatile dispatchShorts default to <aside> with float: right; :::youtube-short directive accepts id attr, url attr, or a bare URL pasted in the body.
fbc1d46progress(playlist, mobile-sidebar, svelte): dynamic YouTube playlist UI with sidebar list and click-to-playFirst-pass playlist UI: build-time Data API fetcher, Astro shell + Svelte island, cross-cutting MobileFeaturePlaceholder, postMessage to iframe via the YouTube IFrame Player API. Svelte integration added to the site for the first time.
12fb61eprogress(playlist): sort by addedAt desc, tighten margins, first attempt at right-escapeCaptured snippet.publishedAt per item; sorted by recency desc; tightened margins. First attempt at the right-escape grid (later refactored).
5a2ad2calmost(playlists): almost there with Youtube playlist rendering in Markdown component pipelineRefactored to the two-sibling architecture: YouTubePlaylistPlayer (in-column card) + YouTubePlaylistScroller (escape-positioned aside) + thin orchestrator. Player and scroller communicate via shared iframeId only.
7b30ba3ship(playlists, ux): youtube playlist embeds now v1 functionalWheel/touchpad scroll fix (root-caused to a broken height chain through MobileFeaturePlaceholder's display: contents). Vertical centering of the scroller against the player. Sticky "Up 5 / Down 5" scroll buttons with edge-disable.

Parent repo (astro-knots on master)

CommitMessageWhat
0e6dc0cdoc(lfm-spec, video-spec): extract Versatile-Component-Library-for-Video-Players, codify S→T→CExtracted the video component family from the LFM spec into its own document. Codified the S→T→C model (Syntax → Trigger → Component) as foundational §3 of the new spec.
7921837changelog(aside-escape): document the pattern + bump submodule for the playlist UI tweak passPublished the aside-escape pattern changelog.
474b967almost(playlists, video-spec): split playlist into orchestrator + sibling components, sync specSpec sync: §4.1.3.3 rewritten as "two sibling elements, not one card"; new §4.1.3.5 codifies the aside-escape CSS recipe; questions 1–2 in §7 marked resolved.
56dc175ship(playlists): bump mpstaton-site submodule for v1 playlist embed UX close-outSubmodule bump for the v1 ship.

What this v1 ships

A bare youtube.com/playlist?list=... URL on its own line in any markdown file — or a :::youtube-playlist container directive with the URL pasted inside, or ::youtube-playlist{id="..."} as a leaf directive — auto-unfurls into:

  • A player card that flows in the article reading column at the column's natural width, with a header strip carrying playlist title, channel, item count, and an "Open on YouTube" CTA.

  • A scroller aside that escape-positions itself in the article's right margin track at ≥1280px viewports (or stacks below at 768–1279px, or surfaces as a full-screen panel below 768px).

  • Build-time data acquisition via the YouTube Data API v3 — playlist metadata, paginated item list, batched per-item durations — cached at src/data/youtube-playlist-cache.json (gitignored) with 30-day TTL. ~3 quota units per playlist on cold cache; free tier covers hundreds of playlists per build.

  • Sort by addedAt descending so the most-recently-added video sits on top, regardless of how the playlist owner has the list ordered on YouTube.

  • Click-to-play via a small Svelte island that hydrates with client:visible, finds the iframe by ID, and uses playVideoAt(N) through the YouTube IFrame Player API.

  • Wheel and touchpad scroll that actually works inside the scroller (today's bug fix), themed with a thin brand-aqua scrollbar.

  • Sticky "Up 5 / Down 5" buttons at the top and bottom of the scroll viewport, auto-disabled at scroll edges.

  • Active-item highlight (brand-aqua left border) that updates as the iframe advances.

  • Vertical centering of the scroller against the player, so when the scroller is taller than the iframe it extends equally above and below.

Two cross-cutting patterns established, available to the next sidebar component

  1. MobileFeaturePlaceholder.astro — the wrapper that lets any sidebar-positioned LFM component degrade gracefully on phones to a placeholder + full-screen panel. Spec §5.9. First consumer is the playlist scroller; future consumers (LinkPreview__Article--Card with aside="right", LinkRollup__Column) inherit the contract.

  2. The aside-escape CSS recipe — position: absolute + left: calc(100% + gap) + width: min(maxw, calc(((100vw - 100%) / 2) - 2rem)) against a position: relative wrapper, lets a child element pop out of a centered article column into the right margin track. Spec §4.1.3.5. The orchestrator pattern (one wrapper composing in-column + escape-positioned siblings) is the reusable shape; MarginEscape.astro extraction waits for a second consumer per the "premature extraction is worse than waiting one round" principle.

Next session

Three forks ahead, pick whichever feels most useful:

  1. Phase 1b — facade mode for YouTubeEmbed, YouTubeShortsEmbed, VimeoEmbed. Reduces first-paint cost on memo pages with multiple embeds. Spec §6.

  2. MarginEscape.astro extraction when a second consumer (likely LinkPreview__Article--Card with aside="right-escape") comes online.

  3. Walk the playlist mobile path on a real phone. Implementation is in place; the swipe thresholds and back-button integration deserve a touch-hardware verification pass.