(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273const API_URL = process.env.API_URL || "http://localhost:8081";
const CRAWLER_AGENTS = [ "facebookexternalhit", "facebot", "twitterbot", "linkedinbot", "whatsapp", "slackbot", "telegrambot", "discordbot", "applebot", "bot", "crawler", "spider", "preview", "cardyb", "bluesky",];
export function isCrawler(userAgent: string): boolean { const ua = userAgent.toLowerCase(); return CRAWLER_AGENTS.some((bot) => ua.includes(bot));}
export interface OGData { title: string; description: string; image: string; author: string; pageURL: string;}
interface APIAnnotation { id?: string; uri?: string; author?: { did: string; handle?: string }; creator?: { did: string; handle?: string }; target?: { source?: string; title?: string; selector?: { exact?: string } }; body?: string; bodyValue?: string; text?: string; motivation?: string; title?: string; description?: string; url?: string; source?: string; selector?: { exact?: string }; selectorJson?: string; color?: string;}
interface APICollection { id?: string; uri?: string; name: string; description?: string; icon?: string; author?: { did: string; handle?: string }; creator?: { did: string; handle?: string };}
export async function resolveHandle(handle: string): Promise<string | null> { if (handle.startsWith("did:")) return handle; try { const res = await fetch( `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`, ); if (!res.ok) return null; const data = await res.json(); return data.did || null; } catch { return null; }}
async function fetchJSON(path: string): Promise<unknown> { const res = await fetch(`${API_URL}${path}`); if (!res.ok) return null; return res.json();}
function getAuthorHandle(item: APIAnnotation | APICollection): string { const author = item.author || item.creator; if (author?.handle) return `@${author.handle}`; if (author?.did) return author.did; return "someone";}
function extractDomain(urlStr: string): string { try { return new URL(urlStr).host; } catch { return ""; }}
function truncate(str: string, max: number): string { if (str.length <= max) return str; return str.slice(0, max - 3) + "...";}
function extractBody(body: unknown): string { if (!body) return ""; if (typeof body === "string") return body; if (typeof body === "object" && body !== null && "value" in body) { return String((body as { value: unknown }).value || ""); } return "";}
const BASE_URL = process.env.BASE_URL || "https://margin.at";
export async function fetchAnnotationOG(uri: string): Promise<OGData | null> { const item = (await fetchJSON( `/api/note?uri=${encodeURIComponent(uri)}`, )) as APIAnnotation | null; if (!item) return null;
const itemURI = item.id || item.uri || uri; const author = getAuthorHandle(item); const source = item.target?.source || item.url || item.source || ""; const domain = extractDomain(source); const selectorText = item.target?.selector?.exact || item.selector?.exact || "";
let title = "Annotation on Margin"; const targetTitle = item.target?.title || item.title; if (targetTitle) title = truncate(`Comment on: ${targetTitle}`, 60);
let description = extractBody(item.body) || item.bodyValue || item.text || ""; if (selectorText && description) { description = `"${truncate(selectorText, 100)}"\n\n${description}`; } else if (selectorText) { description = `Highlighted: "${truncate(selectorText, 150)}"`; } if (!description) { description = `An annotation by ${author}`; if (domain) description += ` on ${domain}`; } description = truncate(description, 200);
return { title, description, image: `${BASE_URL}/og-image?uri=${encodeURIComponent(itemURI)}`, author, pageURL: `${BASE_URL}/at/${encodeURIComponent(itemURI.slice(5))}`, };}
export async function fetchHighlightOG(uri: string): Promise<OGData | null> { const item = (await fetchJSON( `/api/note?uri=${encodeURIComponent(uri)}`, )) as APIAnnotation | null; if (!item) return null;
const itemURI = item.id || item.uri || uri; const author = getAuthorHandle(item); const source = item.target?.source || item.url || item.source || ""; const domain = extractDomain(source); const selectorText = item.target?.selector?.exact || item.selector?.exact || "";
let title = "Highlight on Margin"; const targetTitle = item.target?.title || item.title; if (targetTitle) title = truncate(`Highlight on: ${targetTitle}`, 60);
let description = ""; if (selectorText) { description = `"${truncate(selectorText, 180)}"`; } if (!description) { description = `A highlight by ${author}`; if (domain) description += ` on ${domain}`; }
return { title, description, image: `${BASE_URL}/og-image?uri=${encodeURIComponent(itemURI)}`, author, pageURL: `${BASE_URL}/at/${encodeURIComponent(itemURI.slice(5))}`, };}
export async function fetchBookmarkOG(uri: string): Promise<OGData | null> { const item = (await fetchJSON( `/api/note?uri=${encodeURIComponent(uri)}`, )) as APIAnnotation | null; if (!item) return null;
const itemURI = item.id || item.uri || uri; const author = getAuthorHandle(item); const source = item.target?.source || item.url || item.source || ""; const domain = extractDomain(source);
const title = item.title || item.target?.title || "Bookmark on Margin"; let description = item.description || extractBody(item.body) || item.bodyValue || ""; if (!description) description = "A saved bookmark on Margin"; if (domain) description += ` from ${domain}`; description = truncate(description, 200);
return { title, description, image: `${BASE_URL}/og-image?uri=${encodeURIComponent(itemURI)}`, author, pageURL: `${BASE_URL}/at/${encodeURIComponent(itemURI.slice(5))}`, };}
export async function fetchCollectionOG(uri: string): Promise<OGData | null> { const item = (await fetchJSON( `/api/collection?uri=${encodeURIComponent(uri)}`, )) as APICollection | null; if (!item) return null;
const itemURI = item.id || item.uri || uri; const author = getAuthorHandle(item); const icon = item.icon || "馃搧"; const title = `${icon} ${item.name}`;
let description; if (item.description) { description = `By ${author} 路 ${truncate(item.description, 170)}`; } else { description = `A collection by ${author}`; }
return { title, description, image: `${BASE_URL}/og-image?uri=${encodeURIComponent(itemURI)}`, author, pageURL: `${BASE_URL}/collection/${encodeURIComponent(itemURI)}`, };}
export async function fetchOGByURI(uri: string): Promise<OGData | null> { if (uri.includes("/at.margin.annotation/")) return fetchAnnotationOG(uri); if (uri.includes("/at.margin.highlight/")) return fetchHighlightOG(uri); if (uri.includes("/at.margin.bookmark/")) return fetchBookmarkOG(uri); if (uri.includes("/at.margin.collection/")) return fetchCollectionOG(uri);
return fetchAnnotationOG(uri);}
export async function fetchOGForRoute( did: string, rkey: string, collectionType?: string,): Promise<OGData | null> { if (collectionType) { const uri = `at://${did}/${collectionType}/${rkey}`; return fetchOGByURI(uri); }
for (const type of [ "at.margin.annotation", "at.margin.highlight", "at.margin.bookmark", ]) { const uri = `at://${did}/${type}/${rkey}`; const data = await fetchOGByURI(uri); if (data) return data; }
const colUri = `at://${did}/at.margin.collection/${rkey}`; return fetchCollectionOG(colUri);}