From 07b4fbc5fb423614aaa7e4ffc0ef31f873dc27b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:06:26 +0000 Subject: [PATCH 1/3] Initial plan From 0f817f15d80578b93c6154ed152dc075f3b6616a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:12:41 +0000 Subject: [PATCH 2/3] feat: add oEmbed support Agent-Logs-Url: https://github.com/streamich/react-embed/sessions/03024238-5624-486d-bf5d-3fe9b9dd6a42 Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- src/ReactEmbed.tsx | 1 + src/blocks/oembed/fetchOEmbed.ts | 105 ++++++++++++++++ src/blocks/oembed/index.tsx | 192 +++++++++++++++++++++++++++++ src/index.tsx | 2 + src/routeToBlock.ts | 3 + src/stories/ReactEmbed.stories.tsx | 33 +++++ 6 files changed, 336 insertions(+) create mode 100644 src/blocks/oembed/fetchOEmbed.ts create mode 100644 src/blocks/oembed/index.tsx diff --git a/src/ReactEmbed.tsx b/src/ReactEmbed.tsx index 0ec1ed065..e010ca913 100644 --- a/src/ReactEmbed.tsx +++ b/src/ReactEmbed.tsx @@ -34,6 +34,7 @@ const defaultBlocks: Blocks = { imgur: React.lazy(() => import('./blocks/imgur')), instagram: React.lazy(() => import('./blocks/instagram')), jsfiddle: React.lazy(() => import('./blocks/jsfiddle')), + oembed: React.lazy(() => import('./blocks/oembed')), pdf: React.lazy(() => import('./blocks/pdf')), reactPlayer: React.lazy(() => import('./blocks/react-player')), replit: React.lazy(() => import('./blocks/replit')), diff --git a/src/blocks/oembed/fetchOEmbed.ts b/src/blocks/oembed/fetchOEmbed.ts new file mode 100644 index 000000000..3c169aafc --- /dev/null +++ b/src/blocks/oembed/fetchOEmbed.ts @@ -0,0 +1,105 @@ +/** oEmbed response as defined by https://oembed.com */ +export interface OEmbedResponse { + type: 'photo' | 'video' | 'link' | 'rich'; + version: string; + title?: string; + author_name?: string; + author_url?: string; + provider_name?: string; + provider_url?: string; + cache_age?: number | string; + thumbnail_url?: string; + thumbnail_width?: number; + thumbnail_height?: number; + width?: number; + height?: number; + /** HTML markup for video/rich types */ + html?: string; + /** Direct URL for photo type */ + url?: string; +} + +interface Provider { + patterns: RegExp[]; + endpoint: string; +} + +const PROVIDERS: Provider[] = [ + { + patterns: [/youtube\.com/, /youtu\.be/], + endpoint: 'https://www.youtube.com/oembed', + }, + { + patterns: [/vimeo\.com/], + endpoint: 'https://vimeo.com/api/oembed.json', + }, + { + patterns: [/twitter\.com/, /x\.com/], + endpoint: 'https://publish.twitter.com/oembed', + }, + { + patterns: [/soundcloud\.com/], + endpoint: 'https://soundcloud.com/oembed', + }, + { + patterns: [/instagram\.com/], + endpoint: 'https://api.instagram.com/oembed/', + }, + { + patterns: [/flickr\.com/], + endpoint: 'https://www.flickr.com/services/oembed/', + }, + { + patterns: [/open\.spotify\.com/], + endpoint: 'https://open.spotify.com/oembed', + }, + { + patterns: [/reddit\.com/], + endpoint: 'https://www.reddit.com/oembed', + }, + { + patterns: [/tiktok\.com/], + endpoint: 'https://www.tiktok.com/oembed', + }, + { + patterns: [/codepen\.io/], + endpoint: 'https://codepen.io/api/oembed', + }, + { + patterns: [/dailymotion\.com/], + endpoint: 'https://www.dailymotion.com/services/oembed', + }, + { + patterns: [/giphy\.com/], + endpoint: 'https://giphy.com/services/oembed', + }, +]; + +/** + * Returns the oEmbed JSON endpoint URL for a given page URL, + * or `null` if no provider is registered for that URL. + */ +export function getOEmbedEndpoint(url: string): string | null { + for (const provider of PROVIDERS) { + if (provider.patterns.some((p) => p.test(url))) { + return `${provider.endpoint}?url=${encodeURIComponent(url)}&format=json`; + } + } + return null; +} + +/** + * Fetches oEmbed metadata for the given page URL. + * Throws if no provider is found or the request fails (e.g. due to CORS). + */ +export async function fetchOEmbed(url: string, signal?: AbortSignal): Promise { + const endpoint = getOEmbedEndpoint(url); + if (!endpoint) { + throw new Error(`No oEmbed provider found for URL: ${url}`); + } + const response = await fetch(endpoint, {signal}); + if (!response.ok) { + throw new Error(`oEmbed request failed with status ${response.status}`); + } + return response.json() as Promise; +} diff --git a/src/blocks/oembed/index.tsx b/src/blocks/oembed/index.tsx new file mode 100644 index 000000000..2a3ea8e83 --- /dev/null +++ b/src/blocks/oembed/index.tsx @@ -0,0 +1,192 @@ +import * as React from 'react'; +import {BlockProps} from '../..'; +import {fetchOEmbed, OEmbedResponse} from './fetchOEmbed'; + +export {fetchOEmbed, getOEmbedEndpoint} from './fetchOEmbed'; +export type {OEmbedResponse} from './fetchOEmbed'; + +export interface OEmbedBlockOptions { + /** + * When `true`, the raw HTML provided by the oEmbed provider (e.g. an + * `