From 469536ab31bd2af382c2dbf2935588e274794259 Mon Sep 17 00:00:00 2001 From: Gandalf Date: Mon, 5 Jan 2026 13:26:44 +0100 Subject: [PATCH] Switch Instagram embeds to iframe-only approach Replace InstagramPlugin (which loads external embed.js script) with InstagramEmbedder that generates iframes directly. Changes: - Add InstagramEmbedder following YouTube/Vimeo/3speak pattern - Register InstagramEmbedder in AssetEmbedder - Remove InstagramPlugin from blog renderer plugins - Add tests for InstagramEmbedder Benefits: - No external scripts loaded (privacy improvement) - Consistent with other video/media embedders - Simpler CSP: only frame-src needed, not script-src - Fixes eager script loading issue for Instagram Relates to #786 --- .../features/post-rendering/lib/renderer.ts | 5 +- .../default/embedder/AssetEmbedder.ts | 4 +- .../embedders/InstagramEmbedder.test.ts | 82 +++++++++++++++++++ .../embedder/embedders/InstagramEmbedder.ts | 46 +++++++++++ 4 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 packages/renderer/src/renderers/default/embedder/embedders/InstagramEmbedder.test.ts create mode 100644 packages/renderer/src/renderers/default/embedder/embedders/InstagramEmbedder.ts diff --git a/apps/blog/features/post-rendering/lib/renderer.ts b/apps/blog/features/post-rendering/lib/renderer.ts index 72c9bf7cc..16c7671fc 100644 --- a/apps/blog/features/post-rendering/lib/renderer.ts +++ b/apps/blog/features/post-rendering/lib/renderer.ts @@ -1,4 +1,4 @@ -import { DefaultRenderer, InstagramPlugin, TablePlugin, TwitterPlugin } from '@hive/renderer'; +import { DefaultRenderer, TablePlugin, TwitterPlugin } from '@hive/renderer'; import { getDoubleSize, proxifyImageUrl } from '@ui/lib/old-profixy'; import imageUserBlocklist from '@hive/ui/config/lists/image-user-blocklist'; @@ -20,7 +20,8 @@ const renderDefaultOptions = { ipfsPrefix: '', assetsWidth: 640, assetsHeight: 480, - plugins: [new TwitterPlugin(), new InstagramPlugin(), new TablePlugin()], + // Note: Instagram uses iframe-only via InstagramEmbedder (no external scripts needed) + plugins: [new TwitterPlugin(), new TablePlugin()], imageProxyFn: (url: string) => getDoubleSize(proxifyImageUrl(url, true).replace(/ /g, '%20')), usertagUrlFn: (account: string) => (basePath ? `${basePath}/@${account}` : `/@${account}`), hashtagUrlFn: (hashtag: string) => (basePath ? `${basePath}/trending/${hashtag}` : `/trending/${hashtag}`), diff --git a/packages/renderer/src/renderers/default/embedder/AssetEmbedder.ts b/packages/renderer/src/renderers/default/embedder/AssetEmbedder.ts index ff1c4d0f9..c77e086ea 100644 --- a/packages/renderer/src/renderers/default/embedder/AssetEmbedder.ts +++ b/packages/renderer/src/renderers/default/embedder/AssetEmbedder.ts @@ -1,6 +1,7 @@ import ow from 'ow'; import {LocalizationOptions} from '../Localization'; import {AbstractEmbedder} from './embedders/AbstractEmbedder'; +import {InstagramEmbedder} from './embedders/InstagramEmbedder'; import {SpotifyEmbedder} from './embedders/SpotifyEmbedder'; import {ThreeSpeakEmbedder} from './embedders/ThreeSpeakEmbedder'; import {TwitchEmbedder} from './embedders/TwitchEmbedder'; @@ -22,7 +23,8 @@ export class AssetEmbedder { new VimeoEmbedder(), new TwitchEmbedder(options), new SpotifyEmbedder(), - new ThreeSpeakEmbedder() + new ThreeSpeakEmbedder(), + new InstagramEmbedder() ]; } diff --git a/packages/renderer/src/renderers/default/embedder/embedders/InstagramEmbedder.test.ts b/packages/renderer/src/renderers/default/embedder/embedders/InstagramEmbedder.test.ts new file mode 100644 index 000000000..27938bec0 --- /dev/null +++ b/packages/renderer/src/renderers/default/embedder/embedders/InstagramEmbedder.test.ts @@ -0,0 +1,82 @@ +import {InstagramEmbedder} from './InstagramEmbedder'; + +describe('InstagramEmbedder', () => { + describe('getEmbedMetadata', () => { + const validPostUrls = [ + 'https://www.instagram.com/p/ABC123defgh/', + 'https://instagram.com/p/ABC123defgh/', + 'https://www.instagram.com/p/ABC123defgh', + 'https://instagram.com/p/ABC123defgh' + ]; + + validPostUrls.forEach((url) => { + it(`should return correct metadata for Instagram post URL: ${url}`, () => { + const embedder = new InstagramEmbedder(); + const result = embedder.getEmbedMetadata({data: url} as HTMLObjectElement); + expect(result).toBeDefined(); + expect(result?.id).toBe('p/ABC123defgh'); + expect(result?.url).toContain('instagram.com/p/ABC123defgh'); + }); + }); + + const validReelUrls = [ + 'https://www.instagram.com/reel/XYZ789abcde/', + 'https://instagram.com/reel/XYZ789abcde/', + 'https://www.instagram.com/reel/XYZ789abcde', + 'https://instagram.com/reel/XYZ789abcde' + ]; + + validReelUrls.forEach((url) => { + it(`should return correct metadata for Instagram reel URL: ${url}`, () => { + const embedder = new InstagramEmbedder(); + const result = embedder.getEmbedMetadata({data: url} as HTMLObjectElement); + expect(result).toBeDefined(); + expect(result?.id).toBe('reel/XYZ789abcde'); + expect(result?.url).toContain('instagram.com/reel/XYZ789abcde'); + }); + }); + + const invalidUrls = [ + 'https://www.instagram.com/', + 'https://www.instagram.com/username/', + 'https://www.instagram.com/p/', + 'https://www.instagram.com/p/short', + 'https://www.instagram.com/stories/username/', + 'https://facebook.com/p/ABC123defgh/', + 'not a url' + ]; + + invalidUrls.forEach((url) => { + it(`should return undefined for invalid URL: ${url}`, () => { + const embedder = new InstagramEmbedder(); + const result = embedder.getEmbedMetadata({data: url} as HTMLObjectElement); + expect(result).toBeUndefined(); + }); + }); + }); + + describe('processEmbed', () => { + it('should generate correct iframe HTML for post', () => { + const embedder = new InstagramEmbedder(); + const result = embedder.processEmbed('p/ABC123defgh', {width: 640, height: 480}); + expect(result).toBe( + '
' + ); + }); + + it('should generate correct iframe HTML for reel', () => { + const embedder = new InstagramEmbedder(); + const result = embedder.processEmbed('reel/XYZ789abcde', {width: 500, height: 600}); + expect(result).toBe( + '
' + ); + }); + }); + + describe('type', () => { + it('should have correct type identifier', () => { + const embedder = new InstagramEmbedder(); + expect(embedder.type).toBe('instagram'); + }); + }); +}); diff --git a/packages/renderer/src/renderers/default/embedder/embedders/InstagramEmbedder.ts b/packages/renderer/src/renderers/default/embedder/embedders/InstagramEmbedder.ts new file mode 100644 index 000000000..319022ce3 --- /dev/null +++ b/packages/renderer/src/renderers/default/embedder/embedders/InstagramEmbedder.ts @@ -0,0 +1,46 @@ +import {Log} from '../../../../Log'; +import {AbstractEmbedder, EmbedMetadata} from './AbstractEmbedder'; + +/** + * Embedder for Instagram posts and reels. + * Converts Instagram URLs to iframe embeds without requiring external scripts. + * + * Supported URL formats: + * - https://www.instagram.com/p/POST_ID/ + * - https://www.instagram.com/reel/REEL_ID/ + * - https://instagram.com/p/POST_ID/ + */ +export class InstagramEmbedder extends AbstractEmbedder { + public type = 'instagram'; + + /** + * Matches Instagram post and reel URLs. + * Valid shortcode: Base64URL characters, typically 11 chars (allow 10-14 for safety) + */ + private static readonly linkRegex = /https?:\/\/(?:www\.)?instagram\.com\/(p|reel)\/([a-zA-Z0-9_-]{10,14})\/?/i; + + public getEmbedMetadata(input: string | HTMLObjectElement): EmbedMetadata | undefined { + const data = typeof input === 'string' ? input : input.data; + try { + const match = data.match(InstagramEmbedder.linkRegex); + if (match && match[2]) { + const type = match[1]; // 'p' or 'reel' + const id = match[2]; + return { + // Store both type and id for embed URL construction + id: `${type}/${id}`, + url: match[0] + }; + } + } catch (error) { + Log.log().error(error); + } + return undefined; + } + + public processEmbed(id: string, size: {width: number; height: number}): string { + // id format is "p/POST_ID" or "reel/REEL_ID" + const embedUrl = `https://www.instagram.com/${id}/embed/`; + return `
`; + } +} -- GitLab