feat(embeds): apply review findings + fix TikTok portrait padding

From the review-agent audit:
- Dailymotion: move off the Sept-2024-deprecated /embed/video path to
  geo.dailymotion.com/player.html.
- Reddit: point at embed.reddit.com (www.redditmedia.com now 301s there).
- Vimeo: parse the unlisted hash (vimeo.com/{id}/{hash}) and pass &h=…, add dnt=1.
- Tidal: layout=gridify + ~275px height for albums/playlists (fixes narrow player).
- YouTube/Shorts: playsinline=1 (iOS keeps playback inline); parse /live/ +
  music.youtube.com.
- Apple Music: /music-video/ renders 16:9 instead of a fixed audio height.
- Re-add a minimal sandbox to all media iframes (omits allow-top-navigation →
  blocks phishing redirects) — defense-in-depth atop the CSP frame-src allowlist.
- Self-resize Instagram + Reddit post embeds via a shared useIframeAutoHeight hook
  (also now covers the Tweet embed; matches platform.x.com origin too); drop the
  fixed 720/480 heights. Cap tweet/post columns at ~550px, centered.

Also from user feedback: TikTok portrait player dropped music_info/description,
which forced TikTok's wide 'video + info panel' layout and left empty space
beside the video — now a clean 9:16 player that fills the box.

Tests 726 pass. CSP frame-src gains embed.reddit.com (separate desktop commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 23:56:47 -04:00
co-authored by Claude Opus 4.8
parent b247a0447c
commit 4de2d233ef
4 changed files with 225 additions and 97 deletions
+23 -7
View File
@@ -5,6 +5,7 @@ import {
getYoutubeShortsId,
isYouTubeShorts,
getVimeoVideoId,
getVimeoParts,
getTikTokVideoId,
isTikTokLink,
tiktokIdFromOembed,
@@ -31,14 +32,21 @@ test('YouTube: watch / youtu.be / embed / shorts', () => {
assert.equal(getYouTubeVideoId('https://youtu.be/dQw4w9WgXcQ?t=42'), 'dQw4w9WgXcQ');
assert.equal(getYouTubeVideoId('https://www.youtube.com/embed/dQw4w9WgXcQ'), 'dQw4w9WgXcQ');
assert.equal(getYouTubeVideoId('https://youtube.com/shorts/abc123DEF_-'), 'abc123DEF_-');
assert.equal(getYouTubeVideoId('https://www.youtube.com/live/abcLIVE123'), 'abcLIVE123');
assert.equal(getYouTubeVideoId('https://music.youtube.com/watch?v=musicId12'), 'musicId12');
assert.equal(getYouTubeVideoId('https://vimeo.com/123'), null);
assert.equal(isYouTubeShorts('https://www.youtube.com/shorts/abc123'), true);
assert.equal(getYoutubeShortsId('https://youtube.com/shorts/abc123'), 'abc123');
});
test('Vimeo', () => {
test('Vimeo (incl. unlisted hash)', () => {
assert.equal(getVimeoVideoId('https://vimeo.com/123456789'), '123456789');
assert.equal(getVimeoVideoId('https://vimeo.com/channels/staffpicks'), null);
assert.deepEqual(getVimeoParts('https://vimeo.com/123456789/abc123'), {
id: '123456789',
hash: 'abc123',
});
assert.ok(parseMediaEmbed('https://vimeo.com/123456789/abc123', 'h')?.embedUrl.includes('h=abc123'));
});
test('TikTok: canonical /video/<id> only', () => {
@@ -111,16 +119,23 @@ test('SoundCloud track detection', () => {
test('buildVideoEmbedUrl: cookie-less YouTube + Vimeo', () => {
assert.equal(
buildVideoEmbedUrl('youtube', 'dQw4w9WgXcQ'),
'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?autoplay=1&rel=0',
'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?autoplay=1&rel=0&playsinline=1',
);
assert.equal(
buildVideoEmbedUrl('vimeo', '42'),
'https://player.vimeo.com/video/42?autoplay=1&dnt=1',
);
assert.equal(
buildVideoEmbedUrl('vimeo', '42', 'h4sh'),
'https://player.vimeo.com/video/42?autoplay=1&dnt=1&h=h4sh',
);
assert.equal(buildVideoEmbedUrl('vimeo', '42'), 'https://player.vimeo.com/video/42?autoplay=1');
});
test('parseMediaEmbed: routes provider + kind, builds embed URLs', () => {
assert.deepEqual(parseMediaEmbed('https://youtube.com/shorts/abc', HOST), {
provider: 'youtube',
kind: 'portrait',
embedUrl: 'https://www.youtube-nocookie.com/embed/abc?autoplay=1&rel=0',
embedUrl: 'https://www.youtube-nocookie.com/embed/abc?autoplay=1&rel=0&playsinline=1',
});
assert.equal(parseMediaEmbed('https://www.youtube.com/watch?v=xyz', HOST)?.kind, 'landscape');
assert.equal(parseMediaEmbed('https://streamable.com/abc', HOST)?.provider, 'streamable');
@@ -162,7 +177,8 @@ test('Tidal: track (audio) vs video (landscape)', () => {
embedUrl: 'https://embed.tidal.com/tracks/12345',
height: 120,
});
assert.equal(getTidalEmbed('https://listen.tidal.com/album/999')?.embedUrl, 'https://embed.tidal.com/albums/999');
assert.equal(getTidalEmbed('https://listen.tidal.com/album/999')?.embedUrl, 'https://embed.tidal.com/albums/999?layout=gridify');
assert.equal(getTidalEmbed('https://listen.tidal.com/album/999')?.height, 275);
assert.equal(getTidalEmbed('https://tidal.com/video/555')?.kind, 'landscape');
assert.equal(getTidalEmbed('https://tidal.com/browse'), null);
});
@@ -177,9 +193,9 @@ test('Instagram: p / reel / tv → embed path', () => {
test('Reddit post embed → redditmedia', () => {
assert.equal(
getRedditPostEmbed('https://www.reddit.com/r/aww/comments/abc123/cute_cat/'),
'https://www.redditmedia.com/r/aww/comments/abc123/?ref_source=embed&ref=share&embed=true&theme=dark',
'https://embed.reddit.com/r/aww/comments/abc123/?ref_source=embed&ref=share&embed=true&theme=dark',
);
assert.equal(getRedditPostEmbed('https://old.reddit.com/r/aww/comments/xyz/'), 'https://www.redditmedia.com/r/aww/comments/xyz/?ref_source=embed&ref=share&embed=true&theme=dark');
assert.equal(getRedditPostEmbed('https://old.reddit.com/r/aww/comments/xyz/'), 'https://embed.reddit.com/r/aww/comments/xyz/?ref_source=embed&ref=share&embed=true&theme=dark');
assert.equal(getRedditPostEmbed('https://www.reddit.com/r/aww/'), null); // subreddit, not a post
});
+61 -23
View File
@@ -18,16 +18,19 @@ export type MediaEmbed = {
// --- YouTube --------------------------------------------------------------
const YOUTUBE_HOSTS = ['www.youtube.com', 'youtube.com', 'm.youtube.com', 'music.youtube.com'];
export function getYouTubeVideoId(url: string): string | null {
try {
const { hostname, pathname, searchParams } = new URL(url);
if (hostname === 'youtu.be') return pathname.slice(1).split('/')[0] || null;
if (hostname === 'www.youtube.com' || hostname === 'youtube.com') {
if (YOUTUBE_HOSTS.includes(hostname)) {
if (pathname === '/watch') return searchParams.get('v');
const embedMatch = pathname.match(/^\/embed\/([A-Za-z0-9_-]+)/);
if (embedMatch) return embedMatch[1];
const shortsMatch = pathname.match(/^\/shorts\/([A-Za-z0-9_-]+)/);
if (shortsMatch) return shortsMatch[1];
const m =
pathname.match(/^\/embed\/([A-Za-z0-9_-]+)/) ||
pathname.match(/^\/live\/([A-Za-z0-9_-]+)/) ||
pathname.match(/^\/shorts\/([A-Za-z0-9_-]+)/);
if (m) return m[1];
}
} catch {
/* ignore */
@@ -58,17 +61,22 @@ export function getYoutubeShortsId(url: string): string | null {
// --- Vimeo ----------------------------------------------------------------
export function getVimeoVideoId(url: string): string | null {
/** Vimeo id + optional private/unlisted hash (vimeo.com/{id}/{hash}). */
export function getVimeoParts(url: string): { id: string; hash?: string } | null {
try {
const { hostname, pathname } = new URL(url);
if (hostname !== 'vimeo.com' && hostname !== 'www.vimeo.com') return null;
const m = pathname.match(/^\/(\d+)/);
return m ? m[1] : null;
const m = pathname.match(/^\/(\d+)(?:\/([0-9a-zA-Z]+))?/);
return m ? { id: m[1], hash: m[2] } : null;
} catch {
return null;
}
}
export function getVimeoVideoId(url: string): string | null {
return getVimeoParts(url)?.id ?? null;
}
// --- TikTok ---------------------------------------------------------------
/** Canonical /video/<id> links only; short links resolve via oEmbed (below). */
@@ -115,9 +123,12 @@ export function tiktokIdFromOembed(data: {
}
export function tiktokPlayerEmbedUrl(id: string): string {
// Pure 9:16 video player. NOTE: music_info/description are intentionally OFF —
// they switch TikTok to a wide "video + info panel" layout that leaves empty
// space beside the video in a portrait box.
return `https://www.tiktok.com/player/v1/${encodeURIComponent(
id,
)}?autoplay=1&music_info=1&description=1&controls=1&progress_bar=1&play_button=1&volume_control=1&fullscreen_button=1&rel=0`;
)}?autoplay=1&controls=1&progress_bar=1&play_button=1&volume_control=1&fullscreen_button=1&rel=0`;
}
// --- Dailymotion ----------------------------------------------------------
@@ -210,15 +221,18 @@ export function isSoundCloudTrack(url: string): boolean {
// --- Apple Music ----------------------------------------------------------
/** music.apple.com/<cc>/album|playlist|song/<slug>/<id>[?i=<songId>] → embed player. */
export function getAppleMusicEmbed(url: string): { embedUrl: string; height: number } | null {
export function getAppleMusicEmbed(
url: string,
): { embedUrl: string; height: number; video: boolean } | null {
try {
const u = new URL(url);
if (u.hostname !== 'music.apple.com' && u.hostname !== 'embed.music.apple.com') return null;
if (!/\/(album|playlist|song|music-video)\//.test(u.pathname)) return null;
const embedUrl = `https://embed.music.apple.com${u.pathname}${u.search}`;
const video = /\/music-video\//.test(u.pathname);
// A single song (?i=… on an album, or a /song/ link) is compact; collections are tall.
const isSong = u.searchParams.has('i') || /\/song\//.test(u.pathname);
return { embedUrl, height: isSong ? 175 : 450 };
return { embedUrl, height: isSong ? 175 : 450, video };
} catch {
return null;
}
@@ -251,10 +265,21 @@ export function getTidalEmbed(
let m = p.match(/^\/track\/(\d+)/);
if (m) return { kind: 'audio', embedUrl: `https://embed.tidal.com/tracks/${m[1]}`, height: 120 };
m = p.match(/^\/album\/(\d+)/);
if (m) return { kind: 'audio', embedUrl: `https://embed.tidal.com/albums/${m[1]}`, height: 400 };
if (m)
// layout=gridify → full-width grid that fills the container (fixes the
// narrow/centered default); ~275px is Tidal's own album embed height.
return {
kind: 'audio',
embedUrl: `https://embed.tidal.com/albums/${m[1]}?layout=gridify`,
height: 275,
};
m = p.match(/^\/playlist\/([0-9a-fA-F-]+)/);
if (m)
return { kind: 'audio', embedUrl: `https://embed.tidal.com/playlists/${m[1]}`, height: 400 };
return {
kind: 'audio',
embedUrl: `https://embed.tidal.com/playlists/${m[1]}?layout=gridify`,
height: 275,
};
m = p.match(/^\/video\/(\d+)/);
if (m) return { kind: 'landscape', embedUrl: `https://embed.tidal.com/videos/${m[1]}` };
} catch {
@@ -288,7 +313,8 @@ export function getRedditPostEmbed(url: string): string | null {
if (h !== 'reddit.com') return null;
const m = u.pathname.match(/^\/r\/([A-Za-z0-9_]+)\/comments\/([A-Za-z0-9]+)/);
if (!m) return null;
return `https://www.redditmedia.com/r/${m[1]}/comments/${m[2]}/?ref_source=embed&ref=share&embed=true&theme=dark`;
// embed.reddit.com is the current host (www.redditmedia.com now 301s here).
return `https://embed.reddit.com/r/${m[1]}/comments/${m[2]}/?ref_source=embed&ref=share&embed=true&theme=dark`;
} catch {
return null;
}
@@ -298,9 +324,15 @@ export function getRedditPostEmbed(url: string): string | null {
const enc = encodeURIComponent;
export function buildVideoEmbedUrl(provider: 'youtube' | 'vimeo', id: string): string {
if (provider === 'vimeo') return `https://player.vimeo.com/video/${enc(id)}?autoplay=1`;
return `https://www.youtube-nocookie.com/embed/${enc(id)}?autoplay=1&rel=0`;
export function buildVideoEmbedUrl(provider: 'youtube' | 'vimeo', id: string, hash?: string): string {
if (provider === 'vimeo') {
// dnt=1 = Do Not Track (no non-essential cookies); h={hash} required for unlisted.
return `https://player.vimeo.com/video/${enc(id)}?autoplay=1&dnt=1${
hash ? `&h=${enc(hash)}` : ''
}`;
}
// playsinline=1 keeps iOS Safari from forcing the native fullscreen player.
return `https://www.youtube-nocookie.com/embed/${enc(id)}?autoplay=1&rel=0&playsinline=1`;
}
/** Spotify compact players (track/episode) are short; collections are taller. */
@@ -321,9 +353,13 @@ export function parseMediaEmbed(url: string, host: string): MediaEmbed | null {
if (ytId)
return { provider: 'youtube', kind: 'landscape', embedUrl: buildVideoEmbedUrl('youtube', ytId) };
const vimeoId = getVimeoVideoId(url);
if (vimeoId)
return { provider: 'vimeo', kind: 'landscape', embedUrl: buildVideoEmbedUrl('vimeo', vimeoId) };
const vimeo = getVimeoParts(url);
if (vimeo)
return {
provider: 'vimeo',
kind: 'landscape',
embedUrl: buildVideoEmbedUrl('vimeo', vimeo.id, vimeo.hash),
};
// NOTE: TikTok is handled by its own card (TikTokEmbedCard) — short "copy-link"
// URLs (vm.tiktok.com, tiktok.com/t/…) need a client-side oEmbed lookup to
@@ -334,7 +370,9 @@ export function parseMediaEmbed(url: string, host: string): MediaEmbed | null {
return {
provider: 'dailymotion',
kind: 'landscape',
embedUrl: `https://www.dailymotion.com/embed/video/${enc(dmId)}?autoplay=1`,
// geo.dailymotion.com is the current player; the old /embed/video path was
// deprecated in Sept 2024.
embedUrl: `https://geo.dailymotion.com/player.html?video=${enc(dmId)}&autoplay=1`,
};
const streamableId = getStreamableId(url);
@@ -378,9 +416,9 @@ export function parseMediaEmbed(url: string, host: string): MediaEmbed | null {
if (apple)
return {
provider: 'applemusic',
kind: 'audio',
kind: apple.video ? 'landscape' : 'audio',
embedUrl: apple.embedUrl,
height: apple.height,
height: apple.video ? undefined : apple.height,
};
const tidal = getTidalEmbed(url);