feat(embeds): add Bluesky, Loom, Kick
From the coverage-review agent's ranked recommendations (clean iframes, ids in
the URL, one CSP host each):
- Bluesky: bsky.app/profile/{authority}/post/{rkey} → embed.bsky.app (rich, self-
resizing like the other post embeds).
- Loom: loom.com/share|embed/{id} → www.loom.com/embed/{id} (16:9).
- Kick: kick.com/{channel} → player.kick.com/{channel} (live channels only; VODs/
clips have no clean embed and fall back to a link).
Parsers unit-tested. CSP frame-src gains embed.bsky.app / www.loom.com /
player.kick.com (desktop + live web, both updated). Needs live verification once
deployed since the embeds themselves can't be exercised from here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -368,6 +368,46 @@ export function getRedditPostEmbed(url: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Loom -----------------------------------------------------------------
|
||||
|
||||
export function getLoomId(url: string): string | null {
|
||||
try {
|
||||
const { hostname, pathname } = new URL(url);
|
||||
if (hostname.replace(/^www\./, '') !== 'loom.com') return null;
|
||||
const m = pathname.match(/^\/(?:share|embed)\/([A-Za-z0-9]+)/);
|
||||
return m ? m[1] : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Kick (live channels only; VODs/clips have no clean iframe) ------------
|
||||
|
||||
export function getKickChannel(url: string): string | null {
|
||||
try {
|
||||
const { hostname, pathname } = new URL(url);
|
||||
if (hostname.replace(/^www\./, '') !== 'kick.com') return null;
|
||||
const parts = pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
|
||||
return parts.length === 1 && /^[A-Za-z0-9_]+$/.test(parts[0]) ? parts[0] : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Bluesky --------------------------------------------------------------
|
||||
|
||||
export function getBlueskyEmbed(url: string): string | null {
|
||||
try {
|
||||
const { hostname, pathname } = new URL(url);
|
||||
if (hostname !== 'bsky.app' && hostname !== 'www.bsky.app') return null;
|
||||
const m = pathname.match(/^\/profile\/([^/]+)\/post\/([A-Za-z0-9]+)/);
|
||||
// authority is a handle or did; embed.bsky.app resolves either.
|
||||
return m ? `https://embed.bsky.app/embed/${m[1]}/app.bsky.feed.post/${m[2]}` : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Embed-URL builders ---------------------------------------------------
|
||||
|
||||
const enc = encodeURIComponent;
|
||||
@@ -479,5 +519,24 @@ export function parseMediaEmbed(url: string, host: string): MediaEmbed | null {
|
||||
const reddit = getRedditPostEmbed(url);
|
||||
if (reddit) return { provider: 'reddit', kind: 'rich', embedUrl: reddit, height: 480 };
|
||||
|
||||
const bsky = getBlueskyEmbed(url);
|
||||
if (bsky) return { provider: 'bluesky', kind: 'rich', embedUrl: bsky, height: 600 };
|
||||
|
||||
const loomId = getLoomId(url);
|
||||
if (loomId)
|
||||
return {
|
||||
provider: 'loom',
|
||||
kind: 'landscape',
|
||||
embedUrl: `https://www.loom.com/embed/${enc(loomId)}`,
|
||||
};
|
||||
|
||||
const kick = getKickChannel(url);
|
||||
if (kick)
|
||||
return {
|
||||
provider: 'kick',
|
||||
kind: 'landscape',
|
||||
embedUrl: `https://player.kick.com/${enc(kick)}?autoplay=true`,
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user