Always embed Vimeo first; link out only on player error

Prefer on-site player.vimeo.com embeds. Use the Vimeo Player API to
detect privacy blocks and fall back to Watch on Vimeo only then.
This commit is contained in:
2026-07-24 12:24:47 -04:00
parent 1b29b1d104
commit 738187d1d4
3 changed files with 161 additions and 39 deletions
+120
View File
@@ -0,0 +1,120 @@
import { useEffect, useRef, useState } from "react";
import { mediaSrc } from "../lib/api";
type Props = {
videoId: string;
title: string;
watchUrl: string;
poster?: string;
};
declare global {
interface Window {
Vimeo?: {
Player: new (
el: HTMLIFrameElement | HTMLElement,
opts?: { id?: string | number; url?: string }
) => {
on: (event: string, cb: (data?: unknown) => void) => void;
destroy: () => void;
};
};
}
}
let playerSdkPromise: Promise<void> | null = null;
function loadVimeoSdk(): Promise<void> {
if (window.Vimeo?.Player) return Promise.resolve();
if (playerSdkPromise) return playerSdkPromise;
playerSdkPromise = new Promise((resolve, reject) => {
const existing = document.querySelector<HTMLScriptElement>(
'script[src="https://player.vimeo.com/api/player.js"]'
);
if (existing) {
existing.addEventListener("load", () => resolve());
existing.addEventListener("error", () => reject(new Error("Vimeo SDK failed")));
if (window.Vimeo?.Player) resolve();
return;
}
const s = document.createElement("script");
s.src = "https://player.vimeo.com/api/player.js";
s.async = true;
s.onload = () => resolve();
s.onerror = () => reject(new Error("Vimeo SDK failed"));
document.body.appendChild(s);
});
return playerSdkPromise;
}
/**
* Always embeds first. Only if Vimeo reports a privacy/error does the UI
* fall back to opening the video on vimeo.com (last resort).
*/
export function VimeoEmbed({ videoId, title, watchUrl, poster }: Props) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [blocked, setBlocked] = useState(false);
const embedSrc = `https://player.vimeo.com/video/${videoId}?title=0&byline=0&portrait=0`;
useEffect(() => {
let player: { destroy: () => void } | null = null;
let cancelled = false;
setBlocked(false);
loadVimeoSdk()
.then(() => {
if (cancelled || !iframeRef.current || !window.Vimeo?.Player) return;
player = new window.Vimeo.Player(iframeRef.current);
player.on("error", (data: unknown) => {
// Privacy / password / unavailable — leave site only as last resort
console.warn("Vimeo player error", data);
if (!cancelled) setBlocked(true);
});
})
.catch(() => {
// SDK load failure still keep iframe; browser may still play
});
return () => {
cancelled = true;
try {
player?.destroy();
} catch {
/* ignore */
}
};
}, [videoId]);
if (blocked) {
return (
<a href={watchUrl} target="_blank" rel="noreferrer" className="project-page__video-link">
{poster ? (
<img src={mediaSrc(poster)} alt="" className="project-page__video-poster" />
) : (
<div className="project-page__video-poster project-page__video-poster--empty" />
)}
<span className="project-page__video-cta">
<span className="project-page__play" aria-hidden>
</span>
Watch on Vimeo
<span className="project-page__video-note">
This video cant be embedded here (Vimeo privacy). Opens on Vimeo as a last resort.
</span>
</span>
</a>
);
}
return (
<iframe
ref={iframeRef}
src={embedSrc}
title={title}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
allowFullScreen
referrerPolicy="strict-origin-when-cross-origin"
/>
);
}
+30 -20
View File
@@ -4,6 +4,7 @@ import Lightbox from "yet-another-react-lightbox";
import "yet-another-react-lightbox/styles.css";
import { api, type Project, mediaSrc } from "../lib/api";
import { usePageTitle } from "../lib/pageTitle";
import { VimeoEmbed } from "../components/VimeoEmbed";
import "./ProjectPage.scss";
export function ProjectPage() {
@@ -101,24 +102,39 @@ export function ProjectPage() {
{videos.length > 0 && (
<div className="project-page__videos">
{videos.map((v) => {
const watchUrl = v.externalUrl || v.url;
// Vimeo "nowhere" privacy stores watch-page URL (no player.vimeo.com) → link card
const showEmbed =
v.videoSource === "youtube" ||
(v.videoSource === "vimeo" && v.url.includes("player.vimeo.com"));
const watchUrl =
v.externalUrl ||
(v.videoSource === "vimeo" && v.videoId
? `https://vimeo.com/${v.videoId}`
: v.url);
const vimeoId =
v.videoId ||
(v.url.match(/vimeo\.com\/(?:video\/)?(\d+)/)?.[1] ?? null);
return (
<div key={v.id} className="project-page__video">
{showEmbed ? (
{v.videoSource === "vimeo" && vimeoId ? (
<VimeoEmbed
videoId={vimeoId}
title={v.caption || project.title}
watchUrl={watchUrl}
poster={poster}
/>
) : v.videoSource === "youtube" ? (
<iframe
src={mediaSrc(v.url)}
title={v.caption || project.title}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
allowFullScreen
referrerPolicy="strict-origin-when-cross-origin"
/>
) : v.videoSource === "self_hosted" ? (
<video src={mediaSrc(v.url)} controls playsInline poster={poster || undefined} />
<video
src={mediaSrc(v.url)}
controls
playsInline
poster={poster || undefined}
/>
) : (
<a
href={watchUrl}
@@ -135,21 +151,15 @@ export function ProjectPage() {
<span className="project-page__play" aria-hidden>
</span>
Watch on {v.videoSource === "vimeo" || watchUrl.includes("vimeo.com")
? "Vimeo"
: v.videoSource === "youtube" || watchUrl.includes("youtu")
? "YouTube"
: "external site"}{" "}
Watch video
</span>
{(v.videoSource === "vimeo" || watchUrl.includes("vimeo.com")) && (
<span className="project-page__video-note">
This clips Vimeo privacy settings block on-site embedding
</span>
)}
</a>
)}
{v.caption && <p>{v.caption}</p>}
{v.caption &&
!String(v.caption).includes("opens on Vimeo") &&
!String(v.caption).includes("embedding disabled") && (
<p>{v.caption}</p>
)}
</div>
);
})}