fix(embeds): resolve TikTok short links via oEmbed + og:url fallback
CI / Build & Quality Checks (push) Successful in 10m51s
CI / Trigger Desktop Build (push) Successful in 9s

TikTok 'copy-link' share URLs (vm.tiktok.com, tiktok.com/t/…) carry no video id
and the homeserver's link preview is bot-walled (generic 'TikTok - Make Your
Day', no og:url/og:image), so they fell through to the static fallback card with
no play button.

New TikTokEmbedCard resolves the id client-side via TikTok's CORS-enabled oEmbed
API on click (keeps the facade privacy model), then plays the player/v1 embed
(portrait, autoplay + full controls + our fullscreen button). Canonical
/video/<id> links skip the lookup. Also added a general og:url fallback so other
short/redirect links resolve to their canonical form when the raw URL doesn't.

Web CSP connect-src gains www.tiktok.com for the oEmbed fetch (desktop already
allows https:). Tests 19/19.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 23:03:44 -04:00
parent 6f15f7f56e
commit b247a0447c
3 changed files with 216 additions and 14 deletions
+20 -3
View File
@@ -6,6 +6,9 @@ import {
isYouTubeShorts,
getVimeoVideoId,
getTikTokVideoId,
isTikTokLink,
tiktokIdFromOembed,
tiktokPlayerEmbedUrl,
getDailymotionId,
getStreamableId,
getTwitchTarget,
@@ -40,10 +43,26 @@ test('Vimeo', () => {
test('TikTok: canonical /video/<id> only', () => {
assert.equal(getTikTokVideoId('https://www.tiktok.com/@user/video/7234567890123456789'), '7234567890123456789');
assert.equal(getTikTokVideoId('https://vm.tiktok.com/ZMabc/'), null); // short link redirects
assert.equal(getTikTokVideoId('https://vm.tiktok.com/ZMabc/'), null); // short link → oEmbed
assert.equal(getTikTokVideoId('https://www.tiktok.com/@user'), null);
});
test('isTikTokLink: canonical + short + vm/vt', () => {
assert.equal(isTikTokLink('https://www.tiktok.com/@user/video/123'), true);
assert.equal(isTikTokLink('https://www.tiktok.com/t/ZTSHuuXWq/'), true);
assert.equal(isTikTokLink('https://vm.tiktok.com/ZMabc/'), true);
assert.equal(isTikTokLink('https://www.tiktok.com/@user'), false); // profile, not a video
assert.equal(isTikTokLink('https://youtube.com/watch?v=x'), false);
assert.equal(parseMediaEmbed('https://www.tiktok.com/@u/video/123', 'h'), null); // handled by its own card
});
test('tiktokIdFromOembed + player url', () => {
assert.equal(tiktokIdFromOembed({ embed_product_id: '7659555276823006478' }), '7659555276823006478');
assert.equal(tiktokIdFromOembed({ html: '<blockquote data-video-id="123456">' }), '123456');
assert.equal(tiktokIdFromOembed({}), null);
assert.ok(tiktokPlayerEmbedUrl('999').startsWith('https://www.tiktok.com/player/v1/999?autoplay=1'));
});
test('Dailymotion + Streamable', () => {
assert.equal(getDailymotionId('https://www.dailymotion.com/video/x8abcde'), 'x8abcde');
assert.equal(getDailymotionId('https://dai.ly/x8abcde'), 'x8abcde');
@@ -104,8 +123,6 @@ test('parseMediaEmbed: routes provider + kind, builds embed URLs', () => {
embedUrl: 'https://www.youtube-nocookie.com/embed/abc?autoplay=1&rel=0',
});
assert.equal(parseMediaEmbed('https://www.youtube.com/watch?v=xyz', HOST)?.kind, 'landscape');
assert.equal(parseMediaEmbed('https://www.tiktok.com/@u/video/123', HOST)?.provider, 'tiktok');
assert.equal(parseMediaEmbed('https://www.tiktok.com/@u/video/123', HOST)?.kind, 'portrait');
assert.equal(parseMediaEmbed('https://streamable.com/abc', HOST)?.provider, 'streamable');
const spotify = parseMediaEmbed('https://open.spotify.com/track/abc', HOST);
assert.equal(spotify?.kind, 'audio');