10 KiB
Context
The site is a static Astro site (SSG) with no framework islands — all client-side interactivity uses vanilla JS via <script is:inline> tags. The existing design system is a dark-theme glassmorphism palette (CSS custom properties in global.css). Video and podcast content is rendered through ContentCard.astro → StandardCard.astro, which currently produce <a> tags linking directly to YouTube/Spotify (target="_blank"). There are no existing modal/dialog components in the codebase.
Content data comes from a pre-built JSON cache (ContentItem type) that contains id, source, url, title, summary, publishedAt, thumbnailUrl, and optional metrics.views. For podcasts, the cache may also include an audioUrl (from RSS enclosure) to enable a first-party audio fallback when a Spotify embed URL is not available.
Umami analytics primarily uses declarative data-umami-event-* attributes on HTML elements. For interactions that need runtime-derived properties (e.g., modal close method), the implementation may use umami.track().
Goals / Non-Goals
Goals:
- Keep users on-site when they click video/podcast content cards by opening an embedded player in a modal instead of navigating to YouTube/Spotify
- Stop media playback reliably when the modal is dismissed (close button, Escape key, backdrop click)
- Maintain the existing card visual layout — only the interaction target changes
- Track modal interactions (open, close, CTA clicks) via Umami using the existing
data-umami-event-*attribute pattern - Maintain WCAG 2.2 AA accessibility (focus trap, keyboard support, aria attributes)
Non-Goals:
- Changing the detail pages (
/videos/[id],/podcast/[id]) — they remain as-is for SEO/sharing - Introducing a JS framework (React/Preact/Svelte) — the modal uses vanilla JS consistent with the rest of the project
- Instagram card behavior — Instagram cards are unaffected (they already use a different embed approach)
- Live view count fetching from YouTube API at modal-open time — views come from the pre-built cache. Live counts are a future enhancement
- Picture-in-picture or mini-player functionality
- Autoplay — the user must initiate playback within the embed
Decisions
1. Use native <dialog> element for the modal
Decision: Use the HTML <dialog> element with showModal() / close().
Rationale: The native <dialog> provides built-in backdrop, focus trapping, Escape-to-close, and aria-modal semantics for free. No framework dependency needed, consistent with the project's vanilla-JS approach. Browser support is universal in evergreen browsers.
Alternatives considered:
- Custom
<div>with manual focus trap + backdrop: More code, more accessibility bugs, no benefit. - Astro island with React/Preact: Adds a build dependency and hydration cost for a single component. Overkill for a modal.
2. Destroy iframe on close to stop playback
Decision: When the modal closes, remove the iframe src (set to about:blank or remove the iframe entirely) rather than using the YouTube IFrame API or Spotify SDK to pause.
Rationale: Both YouTube and Spotify embeds load third-party scripts that are outside our control. The YouTube IFrame API requires loading https://www.youtube.com/iframe_api and managing a YT.Player instance. Spotify has no public embed API for programmatic pause. Removing/blanking the iframe src is the simplest, most reliable way to guarantee playback stops — no race conditions, no API loading, works for both platforms identically.
Alternatives considered:
- YouTube IFrame API
player.stopVideo(): Requires async API load, player ready callbacks, and doesn't work for Spotify. Two different code paths for two platforms. postMessageto iframe: Relies on undocumented/unstable message formats per platform. Fragile.
3. Cards become <button> triggers instead of <a> links
Decision: For video/podcast content cards, change the root element from <a href="..." target="_blank"> to a <button> (or a clickable <div> with role="button" tabindex="0") that opens the modal. The card visual stays the same.
Rationale: The card no longer navigates anywhere — it opens a modal. Semantically, this is a button action, not a link. Using a <button> (or button role) is correct for accessibility and avoids the confusion of an <a> that doesn't navigate.
Implementation note: StandardCard.astro currently renders an <a>. A new prop (e.g., as="button") or a separate StandardCardButton.astro wrapper can handle this. The card must carry the ContentItem data as data-* attributes so the modal script can read them on click.
Alternatives considered:
- Keep as
<a>withhref="#"andpreventDefault: Semantically incorrect, accessibility tools may announce it as a link. - Wrap card in both
<a>(for SEO/no-JS fallback) and add JS override: Adds complexity. The detail pages already serve the SEO purpose.
4. Data flow via data-* attributes on card elements
Decision: Cards embed the ContentItem data they need (id, source, url, title, summary, publishedAt, thumbnailUrl, views) as data-* attributes on the card element. The modal script reads these attributes on click to populate the modal.
Rationale: The site is SSG — no runtime data store or state management. data-* attributes are the standard Astro pattern for passing server-rendered data to inline scripts (same approach used for nav toggle with data-nav-toggle, data-open). No serialization overhead, no global state, works with any number of cards.
Alternatives considered:
- Global JSON blob in a
<script>tag: Works but couples the modal to a specific data shape and page.data-*is more composable. - Re-fetch from cache at runtime: The site is static. There's no runtime API to call.
5. Embed URL construction
Decision:
- YouTube: Extract video ID from
item.url(pattern:youtube.com/watch?v={id}) → embed URL:https://www.youtube.com/embed/{id}?rel=0&modestbranding=1 - Spotify/Podcast: When a Spotify episode ID is available, embed using
https://open.spotify.com/embed/episode/{id}with Spotify's recommended iframe allowlist and sizing (fixed height ~232px). If a Spotify embed URL is not available, fall back to an in-modal HTML5<audio>player when anaudioUrlis present, otherwise show an outbound "Listen on Spotify" link.
Rationale: YouTube embed URLs have a stable, well-documented format. Spotify embed works for episodes hosted on Spotify. For non-Spotify podcast URLs, embedding isn't universally possible, so the modal gracefully degrades to showing metadata + a link.
In practice, many podcast feeds provide a creator/profile URL (or a podcasters.spotify.com URL) plus an MP3 enclosure, but not the canonical open.spotify.com/episode/{id} URL needed for a Spotify embed. The audioUrl fallback keeps playback on-site in that case.
6. Umami event taxonomy for modal interactions
Decision: Introduce a new event name media_preview for card clicks that open the modal (replacing outbound_click). Add distinct events for modal interactions:
| Interaction | Event Name | Key Properties |
|---|---|---|
| Card click → modal opens | media_preview |
target_id, placement, title, type, source |
| Modal close (X / Escape / backdrop) | media_preview_close |
target_id, close_method (button / escape / backdrop) |
| "View on YouTube" / "Listen on Spotify" CTA | cta_click |
target_id, placement=media_modal, platform, target_url |
| "Subscribe on YouTube" / "Follow on Spotify" CTA | cta_click |
target_id, placement=media_modal, platform, target_url |
Rationale: outbound_click is no longer accurate — the user isn't leaving the site. media_preview describes what actually happens. Close-method tracking (close_method property) allows measuring whether users engage with the content or immediately dismiss. CTA clicks inside the modal reuse the existing cta_click event name with placement=media_modal for consistency with the CTA tracking taxonomy.
7. Modal layout mirrors the card but expanded
Decision: The modal content area uses the same information architecture as the card (image/embed → title → summary → footer) but expanded:
- Header row: Title (left) + Close button (right)
- Embed area: YouTube iframe or Spotify embed (16:9 aspect ratio for video, fixed height for podcast)
- Body: Full description/summary (not truncated), date, view count
- Footer CTAs: "Subscribe on YouTube" / "Follow on Spotify" + "View on YouTube" / "Listen on Spotify"
Rationale: This mirrors the user's request for "an extended version of the card layout." The familiar structure reduces cognitive load.
Risks / Trade-offs
- [Spotify embed availability] → Not all podcast episodes have Spotify episode URLs. Mitigation: use first-party
<audio>playback from RSS enclosure (audioUrl) when available; otherwise show a "Listen on Spotify" outbound link. Optional: maintain a small override map (content/podcast-spotify-map.json) to supply episode embed URLs when desired. - [Third-party embed loading time] → YouTube/Spotify iframes add load time. Mitigation: iframes are only loaded when the modal opens (lazy). A loading placeholder is shown until the iframe loads.
- [No-JS fallback] → If JavaScript is disabled, the
<button>cards won't open a modal. Mitigation: Include a<noscript>hint or make the card a link as a no-JS fallback (progressive enhancement). The detail pages (/videos/[id],/podcast/[id]) always exist as a non-JS path. - [Content Security Policy] → Embedding YouTube/Spotify requires
frame-srcto allow these domains. Mitigation: verify CSP headers (if any) allowyoutube.comandspotify.comiframe sources. Currently no CSP is set, so no issue. - [Modal on mobile] → Full-screen modal on small viewports may feel heavy. Mitigation: on mobile breakpoints, the modal takes full viewport width with reduced padding, and the embed scales responsively via
aspect-ratioor percentage-based sizing.