Something went wrong. Try again.
A Bsky-like frontend using the atprotocol natively.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113export interface OgImage { url: string; alt?: string; width?: number; height?: number;}
export interface OgVideo { url: string; mimeType?: string; width?: number; height?: number; isGif?: boolean;}
export interface OgData { url: string; title: string; description: string; image?: string; imageAlt?: string; images?: OgImage[]; video?: OgVideo; siteName?: string; type?: 'website' | 'article' | 'profile' | 'video.other';}
export function renderOgHtml(data: OgData): string { const siteName = data.siteName ?? 'Foxsky'; const type = data.type ?? 'website';
const allImages: OgImage[] = data.images?.length ? data.images : data.image ? [{ url: data.image, alt: data.imageAlt }] : [];
const imageMeta = allImages .map((img) => { let tag = ` <meta property="og:image" content="${esc(img.url)}" />`; if (img.alt) tag += `\n <meta property="og:image:alt" content="${esc(img.alt)}" />`; if (img.width) tag += `\n <meta property="og:image:width" content="${img.width}" />`; if (img.height) tag += `\n <meta property="og:image:height" content="${img.height}" />`; return tag; }) .join('\n');
const twitterImage = allImages[0] ? `\n <meta name="twitter:image" content="${esc(allImages[0].url)}" />\n <meta name="twitter:image:alt" content="${esc(allImages[0].alt ?? '')}" />` : '';
const twitterDomain = extractDomain(data.url);
const videoMeta = data.video ? ` <meta property="og:video" content="${esc(data.video.url)}" />\n <meta property="og:video:url" content="${esc(data.video.url)}" />\n <meta property="og:video:type" content="${esc(data.video.mimeType ?? (data.video.isGif ? 'video/gif' : 'video/mp4'))}" />${ data.video.width ? `\n <meta property="og:video:width" content="${data.video.width}" />` : '' }${data.video.height ? `\n <meta property="og:video:height" content="${data.video.height}" />` : ''}${ data.video.isGif ? `\n <meta property="og:video:secure_url" content="${esc(data.video.url)}" />` : '' }\n <meta name="twitter:player" content="${esc(data.video.url)}" />\n <meta name="twitter:player:stream" content="${esc(data.video.url)}" />\n <meta name="twitter:player:stream:content_type" content="${esc(data.video.mimeType ?? (data.video.isGif ? 'video/gif' : 'video/mp4'))}" />` : '';
let twitterCard = 'summary'; if (data.video?.isGif) twitterCard = 'summary_large_image'; else if (data.video) twitterCard = 'player'; else if (allImages.length > 0) twitterCard = 'summary_large_image';
return `<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>${esc(data.title)}</title> <meta name="description" content="${esc(data.description)}" /> <meta property="og:type" content="${esc(type)}" /> <meta property="og:url" content="${esc(data.url)}" /> <meta property="og:title" content="${esc(data.title)}" /> <meta property="og:description" content="${esc(data.description)}" /> <meta property="og:site_name" content="${esc(siteName)}" />${imageMeta}${twitterImage ? '\n' + twitterImage : ''}${videoMeta ? '\n' + videoMeta : ''} <meta name="twitter:card" content="${twitterCard}" /> <meta property="twitter:domain" content="${esc(twitterDomain)}" /> <meta property="twitter:url" content="${esc(data.url)}" /> <meta name="twitter:title" content="${esc(data.title)}" /> <meta name="twitter:description" content="${esc(data.description)}" /></head><body> <script> // Regular browsers get redirected to the SPA window.location.replace(${JSON.stringify(data.url)}); </script> <noscript> <a href="${esc(data.url)}">${esc(data.title)}</a> </noscript></body></html>`;}
function esc(s: string): string { return s .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"');}
function extractDomain(url: string): string { try { return new URL(url).hostname; } catch { return url; }}