/** Open Graph / oEmbed HTML for `/:actor/:name`, `/:actor/:name/embed`, and the oEmbed JSON * endpoint, where `:name` is the project's slug when it has one, else its rkey. Social crawlers * don't run JavaScript, so these pages carry the metadata in plain tags; a human is sent on to * the app (D22). Every interpolated value is HTML-escaped. */ export interface ProjectCardData { /** The handle or DID as given in the URL, used to build the app link verbatim. */ actor: string; did: string; rkey: string; slug?: string; name: string; description?: string; /** A `com.atproto.sync.getBlob` URL for the thumbnail, already resolved by the caller. */ thumbnailUrl?: string; } export function escapeHtml(value: string): string { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function document(options: { title: string; description?: string; thumbnailUrl?: string; pageUrl: string; oembedUrl: string; body: string; }): string { const title = escapeHtml(options.title); const description = options.description ? escapeHtml(options.description) : undefined; const image = options.thumbnailUrl ? escapeHtml(options.thumbnailUrl) : undefined; const pageUrl = escapeHtml(options.pageUrl); const oembedUrl = escapeHtml(options.oembedUrl); return ` ${title} ${description ? `\n` : ''}${image ? `\n` : ''} ${options.body} `; } function projectName(data: ProjectCardData): string { return data.slug ?? data.rkey; } function projectPageUrl(appOrigin: string, actor: string, name: string): string { return `${appOrigin}/${actor}/${name}`; } function embedPageUrl(appOrigin: string, actor: string, name: string): string { return `${appOrigin}/${actor}/${name}/embed`; } function oembedJsonUrl(appOrigin: string, actor: string, name: string): string { const url = new URL('/oembed', appOrigin); url.searchParams.set('url', projectPageUrl(appOrigin, actor, name)); return url.toString(); } /** `/:actor/:name`: a plain redirect for humans via ``, with the og * tags above it for crawlers that never run the refresh. */ export function renderProjectPage(data: ProjectCardData, appOrigin: string): string { const name = projectName(data); const appUrl = projectPageUrl(appOrigin, data.actor, name); const escapedAppUrl = escapeHtml(appUrl); return document({ title: data.name, description: data.description, thumbnailUrl: data.thumbnailUrl, pageUrl: appUrl, oembedUrl: oembedJsonUrl(appOrigin, data.actor, name), body: `\n

Redirecting to ${escapeHtml(data.name)}…

`, }); } /** `/:actor/:name/embed`: an iframe-friendly shell that boots the app in embed mode. The page * itself is the embed; nothing navigates it away. */ export function renderEmbedPage(data: ProjectCardData, appOrigin: string): string { const name = projectName(data); const embedSrc = embedPageUrl(appOrigin, data.actor, name); const escapedSrc = escapeHtml(embedSrc); return document({ title: data.name, description: data.description, thumbnailUrl: data.thumbnailUrl, pageUrl: embedSrc, oembedUrl: oembedJsonUrl(appOrigin, data.actor, name), body: ``, }); } export interface OEmbedJson { type: 'rich'; version: '1.0'; provider_name: 'nibling.dev'; title: string; html: string; width: number; height: number; } /** `GET /oembed?url=...`. `actor`/`rkey` are the ones the caller parsed out of `url`. */ export function renderOEmbedJson(data: ProjectCardData, appOrigin: string): OEmbedJson { const embedSrc = embedPageUrl(appOrigin, data.actor, projectName(data)); const width = 600; const height = 400; return { type: 'rich', version: '1.0', provider_name: 'nibling.dev', title: data.name, html: ``, width, height, }; }