Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132/** 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, '"') .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 `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title><meta property="og:title" content="${title}">${description ? `<meta property="og:description" content="${description}">\n` : ''}${image ? `<meta property="og:image" content="${image}">\n` : ''}<meta property="og:url" content="${pageUrl}"><meta name="twitter:card" content="${image ? 'summary_large_image' : 'summary'}"><link rel="alternate" type="application/json+oembed" href="${oembedUrl}" title="${title}"></head><body>${options.body}</body></html>`;}
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 `<meta http-equiv="refresh">`, 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: `<meta http-equiv="refresh" content="0; url=${escapedAppUrl}">\n<p>Redirecting to <a href="${escapedAppUrl}">${escapeHtml(data.name)}</a>…</p>`, });}
/** `/: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: `<iframe src="${escapedSrc}" style="position:fixed;inset:0;width:100%;height:100%;border:0" allow="cross-origin-isolated" title="${escapeHtml(data.name)}"></iframe>`, });}
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: `<iframe src="${escapeHtml(embedSrc)}" width="${width}" height="${height}" style="border:0" allow="cross-origin-isolated"></iframe>`, width, height, };}