diff --git a/src/app/components/url-preview/TikTokEmbed.css.tsx b/src/app/components/url-preview/TikTokEmbed.css.tsx index 7513143..7ad6c32 100644 --- a/src/app/components/url-preview/TikTokEmbed.css.tsx +++ b/src/app/components/url-preview/TikTokEmbed.css.tsx @@ -112,5 +112,7 @@ export const TikTokEmbedContainer = style([ alignItems: 'center', justifyContent: 'center', backgroundColor: '#000', + border: 'none', + display: 'block', }, ]); diff --git a/src/app/components/url-preview/TikTokEmbed.tsx b/src/app/components/url-preview/TikTokEmbed.tsx index 00c33d7..aa2bca7 100644 --- a/src/app/components/url-preview/TikTokEmbed.tsx +++ b/src/app/components/url-preview/TikTokEmbed.tsx @@ -42,6 +42,12 @@ export function isTikTokUrl(url: string): boolean { return TIKTOK_URL_PATTERNS.some((pattern) => pattern.test(url)); } +function extractTikTokVideoId(url: string, embedHtml = ''): string | null { + const urlMatch = url.match(/\/video\/(\d+)/i) ?? url.match(/\/v\/(\d+)/i); + const embedMatch = embedHtml.match(/data-video-id=["'](\d+)["']/i); + return urlMatch?.[1] ?? embedMatch?.[1] ?? null; +} + /** * Fetches TikTok video metadata using oEmbed API * @param url The TikTok video URL @@ -53,29 +59,39 @@ async function fetchTikTokOEmbed(url: string): Promise<{ authorUrl: string; thumbnailUrl: string; embedHtml: string; + videoId: string | null; } | null> { try { const oembedUrl = `${TIKTOK_OEMBED_API}?url=${encodeURIComponent(url)}`; const response = await fetch(oembedUrl); if (!response.ok) return null; - + const data = await response.json(); - + return { title: data.title || 'TikTok Video', authorName: data.author_name || 'Unknown', authorUrl: data.author_url || url, thumbnailUrl: data.thumbnail_url || '', embedHtml: data.html || '', + videoId: extractTikTokVideoId(url, data.html || ''), }; } catch { return null; } } -type TikTokEmbedState = +type TikTokEmbedState = | { status: 'loading' } - | { status: 'loaded'; title: string; authorName: string; authorUrl: string; thumbnailUrl: string; embedHtml: string } + | { + status: 'loaded'; + title: string; + authorName: string; + authorUrl: string; + thumbnailUrl: string; + embedHtml: string; + videoId: string | null; + } | { status: 'error' }; type TikTokEmbedProps = { @@ -88,7 +104,7 @@ type TikTokEmbedProps = { */ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref) => { const [state, setState] = useState({ status: 'loading' }); - const [showEmbed, setShowEmbed] = useState(false); + const [showPlayer, setShowPlayer] = useState(false); // Fetch video info on mount useEffect(() => { @@ -96,7 +112,7 @@ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref) fetchTikTokOEmbed(url).then((info) => { if (cancelled) return; - + if (info) { setState({ status: 'loaded', @@ -105,6 +121,7 @@ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref) authorUrl: info.authorUrl, thumbnailUrl: info.thumbnailUrl, embedHtml: info.embedHtml, + videoId: info.videoId, }); } else { setState({ status: 'error' }); @@ -134,9 +151,9 @@ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref) TikTok - Loading... - @@ -169,7 +186,7 @@ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref) } // Loaded state - show thumbnail or embed - if (!showEmbed) { + if (!showPlayer) { return (
@@ -203,23 +220,27 @@ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref) ); } - // Show embed (using dangerouslySetInnerHTML for TikTok's oEmbed HTML) + const playerUrl = state.videoId + ? `https://www.tiktok.com/player/v1/${state.videoId}?autoplay=1` + : null; + + // Use TikTok's documented player instead of injecting the oEmbed script. return (
@@ -236,10 +257,20 @@ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref) {state.title}
-
+ {playerUrl ? ( +