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.

at trunk 2.2 kB View raw
1/** 2 * Linkify a writer's Bluesky bio the way the Bluesky client does. 3 * 4 * `app.bsky.actor.profile.description` is plain text — profile records carry no `facets` 5 * (unlike `app.bsky.feed.post`). bsky.app computes the links at render time via 6 * `RichText.detectFacets()`. We do the same with `detectFacetsWithoutResolution()`, which 7 * needs no network: we link mentions by handle (not DID), so nothing has to be resolved. 8 * That also keeps this off the SSRF-guarded fetch path entirely. (Decision 0015.) 9 * 10 * Returns ordered segments; the caller renders them with auto-escaping, never `set:html`. 11 */ 12import { RichText } from '@atproto/api'; 13import { atmosphereProfileUrl, atmosphereHashtagUrl } from '../social/atmosphere-url'; 14 15export type BioSegment = 16 | { type: 'text'; text: string } 17 | { type: 'link'; text: string; href: string }; 18 19/** 20 * The original `uri` if it is an http(s) URL, else null. `detectFacets` should only ever 21 * produce http(s) links, but this guarantees a hostile PDS can't smuggle e.g. a 22 * `javascript:` URL through. Returns the raw uri (not a normalised `URL.href`) so display 23 * fidelity matches what the writer typed. 24 */ 25export function safeHttpHref( uri: string ): string | null { 26 try { 27 const { protocol } = new URL( uri ); 28 return protocol === 'http:' || protocol === 'https:' ? uri : null; 29 } catch { 30 return null; 31 } 32} 33 34export function detectBioSegments( description: string ): BioSegment[] { 35 if ( ! description.trim() ) { 36 return []; 37 } 38 39 const richText = new RichText( { text: description } ); 40 richText.detectFacetsWithoutResolution(); 41 42 const segments: BioSegment[] = []; 43 for ( const segment of richText.segments() ) { 44 let href: string | null = null; 45 46 if ( segment.isLink() && segment.link ) { 47 href = safeHttpHref( segment.link.uri ); 48 } else if ( segment.isMention() ) { 49 const handle = segment.text.replace( /^@/, '' ); 50 href = handle ? atmosphereProfileUrl( handle ) : null; 51 } else if ( segment.isTag() ) { 52 const tag = segment.text.replace( /^#/, '' ); 53 href = tag ? atmosphereHashtagUrl( tag ) : null; 54 } 55 56 segments.push( 57 href 58 ? { type: 'link', text: segment.text, href } 59 : { type: 'text', text: segment.text } 60 ); 61 } 62 63 return segments; 64}