embed.tsx
1 import styles from './embed.module.css'; 2 3 interface EmbedProps { 4 url: string; 5 } 6 7 const Embed = ({ url }: EmbedProps) => { 8 const parsedUrl = new URL(url); 9 10 if (youtubeHosts.has(parsedUrl.host) || (parsedUrl.host.startsWith('yt.') && parsedUrl.searchParams.has('v'))) { 11 return <YoutubeEmbed parsedUrl={parsedUrl} />; 12 } 13 if (xHosts.has(parsedUrl.host)) { 14 return <XEmbed parsedUrl={parsedUrl} />; 15 } 16 if (redditHosts.has(parsedUrl.host)) { 17 return <RedditEmbed parsedUrl={parsedUrl} />; 18 } 19 if (twitchHosts.has(parsedUrl.host)) { 20 return <TwitchEmbed parsedUrl={parsedUrl} />; 21 } 22 if (tiktokHosts.has(parsedUrl.host)) { 23 return <TiktokEmbed parsedUrl={parsedUrl} />; 24 } 25 if (instagramHosts.has(parsedUrl.host)) { 26 return <InstagramEmbed parsedUrl={parsedUrl} />; 27 } 28 if (odyseeHosts.has(parsedUrl.host)) { 29 return <OdyseeEmbed parsedUrl={parsedUrl} />; 30 } 31 if (bitchuteHosts.has(parsedUrl.host)) { 32 return <BitchuteEmbed parsedUrl={parsedUrl} />; 33 } 34 if (streamableHosts.has(parsedUrl.host)) { 35 return <StreamableEmbed parsedUrl={parsedUrl} />; 36 } 37 if (spotifyHosts.has(parsedUrl.host)) { 38 return <SpotifyEmbed parsedUrl={parsedUrl} />; 39 } 40 if (soundcloudHosts.has(parsedUrl.host)) { 41 return <SoundcloudEmbed parsedUrl={parsedUrl} />; 42 } 43 }; 44 45 interface EmbedComponentProps { 46 parsedUrl: URL; 47 } 48 49 const youtubeHosts = new Set<string>(['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be', 'm.youtube.com', 'music.youtube.com']); 50 51 const YoutubeEmbed = ({ parsedUrl }: EmbedComponentProps) => { 52 let embedSrc = ''; 53 54 if (parsedUrl.searchParams.has('list')) { 55 const playlistId = parsedUrl.searchParams.get('list'); 56 embedSrc = `https://www.youtube.com/embed/videoseries?list=${playlistId}`; 57 } else { 58 let videoId = parsedUrl.searchParams.get('v'); 59 60 if (!videoId && parsedUrl.host.includes('youtu.be')) { 61 videoId = parsedUrl.pathname.substring(1); 62 } 63 64 if (videoId) { 65 embedSrc = `https://www.youtube.com/embed/${videoId}`; 66 } 67 } 68 69 if (embedSrc) { 70 return ( 71 <iframe 72 className={styles.videoEmbed} 73 height='100%' 74 width='100%' 75 referrerPolicy='origin' 76 allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share' 77 allowFullScreen 78 title={parsedUrl.href} 79 src={embedSrc} 80 /> 81 ); 82 } 83 return null; 84 }; 85 86 const xHosts = new Set<string>(['twitter.com', 'www.twitter.com', 'x.com', 'www.x.com']); 87 88 const XEmbed = ({ parsedUrl }: EmbedComponentProps) => { 89 return ( 90 <iframe 91 className={styles.xEmbed} 92 height='100%' 93 width='100%' 94 referrerPolicy='no-referrer' 95 allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share' 96 title={parsedUrl.href} 97 srcDoc={` 98 <blockquote class="twitter-tweet" data-theme="dark"> 99 <a href="${parsedUrl.href.replace('x.com', 'twitter.com')}"></a> 100 </blockquote> 101 <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 102 `} 103 /> 104 ); 105 }; 106 107 const redditHosts = new Set<string>(['reddit.com', 'www.reddit.com', 'old.reddit.com']); 108 109 const RedditEmbed = ({ parsedUrl }: EmbedComponentProps) => { 110 return ( 111 <iframe 112 className={styles.redditEmbed} 113 height='100%' 114 width='100%' 115 referrerPolicy='no-referrer' 116 allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share' 117 title={parsedUrl.href} 118 srcDoc={` 119 <style> 120 /* fix reddit iframe being centered */ 121 iframe { 122 margin: 0!important; 123 } 124 </style> 125 <blockquote class="reddit-embed-bq" style="height:240px" data-embed-theme="dark"> 126 <a href="${parsedUrl.href}"></a> 127 </blockquote> 128 <script async src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script> 129 `} 130 /> 131 ); 132 }; 133 134 const twitchHosts = new Set<string>(['twitch.tv', 'www.twitch.tv']); 135 136 const TwitchEmbed = ({ parsedUrl }: EmbedComponentProps) => { 137 let iframeUrl; 138 if (parsedUrl.pathname.startsWith('/videos/')) { 139 const videoId = parsedUrl.pathname.replace('/videos/', ''); 140 iframeUrl = `https://player.twitch.tv/?video=${videoId}&parent=${window.location.hostname}`; 141 } else { 142 const channel = parsedUrl.pathname.replaceAll('/', ''); 143 iframeUrl = `https://player.twitch.tv/?channel=${channel}&parent=${window.location.hostname}`; 144 } 145 return ( 146 <iframe 147 className={styles.videoEmbed} 148 height='100%' 149 width='100%' 150 referrerPolicy='no-referrer' 151 allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share' 152 allowFullScreen 153 title={parsedUrl.href} 154 src={iframeUrl} 155 /> 156 ); 157 }; 158 159 const tiktokHosts = new Set<string>(['tiktok.com', 'www.tiktok.com']); 160 161 const TiktokEmbed = ({ parsedUrl }: EmbedComponentProps) => { 162 const videoId = parsedUrl.pathname.replace(/.+\/video\//, '').replaceAll('/', ''); 163 return ( 164 <iframe 165 className={styles.tiktokEmbed} 166 height='100%' 167 width='100%' 168 referrerPolicy='no-referrer' 169 allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share' 170 title={parsedUrl.href} 171 srcDoc={` 172 <blockquote class="tiktok-embed" data-video-id="${videoId}"> 173 <a></a> 174 </blockquote> 175 <script async src="https://www.tiktok.com/embed.js"></script> 176 `} 177 /> 178 ); 179 }; 180 181 const instagramHosts = new Set<string>(['instagram.com', 'www.instagram.com']); 182 183 const InstagramEmbed = ({ parsedUrl }: EmbedComponentProps) => { 184 const pathNames = parsedUrl.pathname.replace(/\/+$/, '').split('/'); 185 const id = pathNames[pathNames.length - 1]; 186 return ( 187 <iframe 188 className={styles.instagramEmbed} 189 height='100%' 190 width='100%' 191 referrerPolicy='no-referrer' 192 allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share' 193 title={parsedUrl.href} 194 srcDoc={` 195 <blockquote class="instagram-media"> 196 <a href="https://www.instagram.com/p/${id}/"></a> 197 </blockquote> 198 <script async src="//www.instagram.com/embed.js"></script> 199 `} 200 /> 201 ); 202 }; 203 204 const odyseeHosts = new Set<string>(['odysee.com', 'www.odysee.com']); 205 206 const OdyseeEmbed = ({ parsedUrl }: EmbedComponentProps) => { 207 const iframeUrl = `https://odysee.com/$/embed${parsedUrl.pathname}`; 208 return ( 209 <iframe 210 className={styles.videoEmbed} 211 height='100%' 212 width='100%' 213 referrerPolicy='no-referrer' 214 allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share' 215 allowFullScreen 216 title={parsedUrl.href} 217 src={iframeUrl} 218 /> 219 ); 220 }; 221 222 const bitchuteHosts = new Set<string>(['bitchute.com', 'www.bitchute.com']); 223 224 const BitchuteEmbed = ({ parsedUrl }: EmbedComponentProps) => { 225 const videoId = parsedUrl.pathname.replace(/\/video\//, '').replaceAll('/', ''); 226 return ( 227 <iframe 228 className={styles.videoEmbed} 229 height='100%' 230 width='100%' 231 referrerPolicy='no-referrer' 232 allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share' 233 allowFullScreen 234 title={parsedUrl.href} 235 src={`https://www.bitchute.com/embed/${videoId}/`} 236 /> 237 ); 238 }; 239 240 const streamableHosts = new Set<string>(['streamable.com', 'www.streamable.com']); 241 242 const StreamableEmbed = ({ parsedUrl }: EmbedComponentProps) => { 243 const videoId = parsedUrl.pathname.replaceAll('/', ''); 244 return ( 245 <iframe 246 className={styles.videoEmbed} 247 height='100%' 248 width='100%' 249 referrerPolicy='no-referrer' 250 allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share' 251 allowFullScreen 252 title={parsedUrl.href} 253 src={`https://streamable.com/e/${videoId}`} 254 /> 255 ); 256 }; 257 258 const spotifyHosts = new Set<string>(['spotify.com', 'www.spotify.com', 'open.spotify.com']); 259 260 const SpotifyEmbed = ({ parsedUrl }: EmbedComponentProps) => { 261 const iframeUrl = `https://open.spotify.com/embed${parsedUrl.pathname}?theme=0`; 262 return ( 263 <iframe 264 className={styles.audioEmbed} 265 height='100%' 266 width='100%' 267 referrerPolicy='no-referrer' 268 allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share' 269 allowFullScreen 270 title={parsedUrl.href} 271 src={iframeUrl} 272 /> 273 ); 274 }; 275 276 const soundcloudHosts = new Set(['soundcloud.com', 'www.soundcloud.com', 'on.soundcloud.com', 'api.soundcloud.com', 'w.soundcloud.com']); 277 278 // not officially documented https://stackoverflow.com/questions/20870270/how-to-get-soundcloud-embed-code-by-soundcloud-com-url 279 const SoundcloudEmbed = ({ parsedUrl }: EmbedComponentProps) => { 280 return ( 281 <iframe 282 className={styles.soundcloudEmbed} 283 height='100%' 284 width='100%' 285 referrerPolicy='no-referrer' 286 allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share' 287 allowFullScreen 288 title={parsedUrl.href} 289 src={`https://w.soundcloud.com/player/?url=${parsedUrl.href}`} 290 /> 291 ); 292 }; 293 294 const canEmbedHosts = new Set<string>([ 295 ...youtubeHosts, 296 ...xHosts, 297 ...redditHosts, 298 ...twitchHosts, 299 ...tiktokHosts, 300 ...instagramHosts, 301 ...odyseeHosts, 302 ...bitchuteHosts, 303 ...soundcloudHosts, 304 ...streamableHosts, 305 ...spotifyHosts, 306 ]); 307 308 export const canEmbed = (parsedUrl: URL): boolean => { 309 return canEmbedHosts.has(parsedUrl.host) || (parsedUrl.host.startsWith('yt.') && parsedUrl.searchParams.has('v')); 310 }; 311 312 export default Embed;