A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1/**
2 * Recognise which embed provider a URL belongs to (the embed content model).
3 *
4 * Dependency-free (no `@wordpress/*`) so both the reader path and the editor can
5 * import it. Hosts are matched at a dot boundary — `host === domain ||
6 * host.endsWith( '.' + domain )` — so look-alikes (`notbsky.app`,
7 * `bsky.app.evil.com`) do NOT match. Mirrors the `detectProvider` pattern
8 * (Decision 0017). A URL we don't recognise returns null → it stays a plain link.
9 */
10export type EmbedKind = 'atproto' | 'youtube' | 'vimeo';
11
12export interface EmbedMatch {
13 kind: EmbedKind;
14 /** atproto: "<authority>/<rkey>" (authority = handle or did). video: the id. */
15 id: string;
16}
17
18/** AppView web hosts that use the standard `/profile/<id>/post/<rkey>` scheme. */
19const ATPROTO_HOSTS = [ 'bsky.app', 'mu.social' ];
20
21function hostMatches( host: string, domain: string ): boolean {
22 const h = host.toLowerCase();
23 return h === domain || h.endsWith( '.' + domain );
24}
25
26/**
27 * Detect whether a URL points at a supported embed provider.
28 *
29 * @param url - The candidate URL (an `https://` AppView/video link or a raw `at://` URI).
30 * @returns The matched embed (`kind` + `id`), or `null` when the URL is unrelated or malformed.
31 */
32export function detectEmbed( url: string ): EmbedMatch | null {
33 // Raw at:// post URI.
34 const at = url.match( /^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/([^/?#]+)/ );
35 if ( at ) {
36 return { kind: 'atproto', id: `${ at[ 1 ] }/${ at[ 2 ] }` };
37 }
38
39 let parsed: URL;
40 try {
41 parsed = new URL( url );
42 } catch {
43 return null;
44 }
45 const host = parsed.hostname;
46
47 if ( ATPROTO_HOSTS.some( ( d ) => hostMatches( host, d ) ) ) {
48 const m = parsed.pathname.match( /^\/profile\/([^/]+)\/post\/([^/?#]+)/ );
49 return m ? { kind: 'atproto', id: `${ m[ 1 ] }/${ m[ 2 ] }` } : null;
50 }
51
52 if ( hostMatches( host, 'youtube.com' ) ) {
53 const v = parsed.searchParams.get( 'v' );
54 return v ? { kind: 'youtube', id: v } : null;
55 }
56 if ( hostMatches( host, 'youtu.be' ) ) {
57 const id = parsed.pathname.slice( 1 ).split( '/' )[ 0 ];
58 return id ? { kind: 'youtube', id } : null;
59 }
60 if ( hostMatches( host, 'vimeo.com' ) ) {
61 const id = parsed.pathname.split( '/' ).filter( Boolean )[ 0 ];
62 return id && /^\d+$/.test( id ) ? { kind: 'vimeo', id } : null;
63 }
64
65 return null;
66}