A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Embeds: resolve quote-post images; iframe a11y title; render safety note

+31 -3
+1
src/lib/blocks/render.ts
··· 76 76 } 77 77 78 78 case 'core/embed': { 79 + // Safe even if a hostile PDS pre-set `_skypressEmbed`: card values are escaped + scheme-guarded in card.ts, video cards carry no iframe, and output is sanitised last. 79 80 const data = node.attributes?._skypressEmbed as EmbedData | undefined; 80 81 if ( data ) { 81 82 return renderEmbedCard( data );
+1
src/lib/embeds/facade.ts
··· 20 20 } 21 21 const iframe = document.createElement( 'iframe' ); 22 22 iframe.setAttribute( 'src', src ); 23 + iframe.setAttribute( 'title', provider === 'youtube' ? 'YouTube video player' : 'Vimeo video player' ); 23 24 iframe.setAttribute( 'allow', 'autoplay; fullscreen; picture-in-picture' ); 24 25 iframe.setAttribute( 'allowfullscreen', 'true' ); 25 26 iframe.setAttribute( 'loading', 'lazy' );
+23
src/lib/embeds/resolve.test.ts
··· 45 45 } ); 46 46 } ); 47 47 48 + it( 'reads images from a recordWithMedia quote-post embed', async () => { 49 + const fetchImpl = vi.fn().mockResolvedValueOnce( 50 + jsonResponse( { 51 + posts: [ 52 + { 53 + uri: 'at://did:plc:abc/app.bsky.feed.post/xyz', 54 + author: { handle: 'a.bsky.social' }, 55 + record: { text: 'quoting' }, 56 + embed: { 57 + $type: 'app.bsky.embed.recordWithMedia#view', 58 + media: { 59 + $type: 'app.bsky.embed.images#view', 60 + images: [ { fullsize: 'https://cdn/q.jpg', alt: 'quoted pic' } ], 61 + }, 62 + }, 63 + }, 64 + ], 65 + } ) 66 + ); 67 + const card = await fetchAtprotoCard( { kind: 'atproto', id: 'did:plc:abc/xyz' }, fetchImpl ); 68 + expect( card?.images ).toEqual( [ { src: 'https://cdn/q.jpg', alt: 'quoted pic' } ] ); 69 + } ); 70 + 48 71 it( 'skips profile resolution when the authority is already a DID', async () => { 49 72 const fetchImpl = vi.fn().mockResolvedValueOnce( 50 73 jsonResponse( {
+6 -3
src/lib/embeds/resolve.ts
··· 19 19 const defaultFetch: FetchLike = ( url ) => safeFetch( url ); 20 20 21 21 function extractImages( embed: unknown ): AtprotoImage[] { 22 - const e = embed as { $type?: string; images?: { fullsize?: string; thumb?: string; alt?: string }[] }; 23 - if ( ! e || ! Array.isArray( e.images ) ) { 22 + type ImageView = { fullsize?: string; thumb?: string; alt?: string }; 23 + const e = embed as { $type?: string; images?: ImageView[]; media?: { images?: ImageView[] } }; 24 + // Direct `images#view` carries `images`; a `recordWithMedia#view` quote-post nests them under `media.images`. 25 + const images = Array.isArray( e?.images ) ? e.images : Array.isArray( e?.media?.images ) ? e.media.images : null; 26 + if ( ! images ) { 24 27 return []; 25 28 } 26 - return e.images 29 + return images 27 30 .map( ( img ) => ( { src: img.fullsize ?? img.thumb ?? '', alt: img.alt ?? '' } ) ) 28 31 .filter( ( img ) => img.src ); 29 32 }