fix(builder): VideoBlock parses more YouTube/Vimeo URL shapes (D4)

Extend detectVideoType to handle youtube.com/shorts/ID, youtube.com/live/ID,
youtube.com/watch?...&v=ID (v not the first query param), and Vimeo
private-hash URLs (vimeo.com/ID/HASH -> ?h=HASH player param). Existing
shapes (youtu.be/ID, youtube.com/embed/ID, youtube.com/watch?v=ID,
vimeo.com/ID, direct files) keep working. The emitted embed src still
passes through safeUrl unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 13:46:48 -07:00
parent cdcc3969bc
commit fdd088b4bf
2 changed files with 93 additions and 7 deletions
+32 -7
View File
@@ -26,18 +26,43 @@ interface VideoBlockProps {
/* ---------- URL detection ---------- */
/**
* Extract a YouTube video ID from any of the URL shapes WHP users paste:
* youtu.be/ID, youtube.com/embed/ID, youtube.com/shorts/ID,
* youtube.com/live/ID (path-based), and youtube.com/watch?...v=ID where `v`
* may appear anywhere in the query string (not just as the first param).
*/
function extractYouTubeId(url: string): string | null {
const pathMatch = url.match(
/(?:youtube\.com\/(?:embed|shorts|live)\/|youtu\.be\/)([a-zA-Z0-9_-]+)/
);
if (pathMatch) return pathMatch[1];
const queryMatch = url.match(/youtube\.com\/watch\?([^\s#]+)/);
if (queryMatch) {
const v = new URLSearchParams(queryMatch[1]).get('v');
if (v) return v;
}
return null;
}
function detectVideoType(url: string): { type: VideoType; embedUrl: string } {
if (!url) return { type: 'none', embedUrl: '' };
// YouTube
const ytMatch = url.match(
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]+)/
);
if (ytMatch) return { type: 'youtube', embedUrl: `https://www.youtube.com/embed/${ytMatch[1]}?rel=0` };
const ytId = extractYouTubeId(url);
if (ytId) return { type: 'youtube', embedUrl: `https://www.youtube.com/embed/${ytId}?rel=0` };
// Vimeo
const vmMatch = url.match(/vimeo\.com\/(\d+)/);
if (vmMatch) return { type: 'vimeo', embedUrl: `https://player.vimeo.com/video/${vmMatch[1]}` };
// Vimeo: vimeo.com/ID, or vimeo.com/ID/HASH for unlisted/private videos
// (the hash becomes the player's `h` query param).
const vmMatch = url.match(/vimeo\.com\/(\d+)(?:\/([a-zA-Z0-9]+))?/);
if (vmMatch) {
const embedUrl = vmMatch[2]
? `https://player.vimeo.com/video/${vmMatch[1]}?h=${vmMatch[2]}`
: `https://player.vimeo.com/video/${vmMatch[1]}`;
return { type: 'vimeo', embedUrl };
}
// Direct file
if (url.match(/\.(mp4|webm|ogg|mov)(\?|$)/i)) return { type: 'file', embedUrl: url };