import { loadBlogFeed, type BlogFeedItem } from "#lib/blog.ts"; import type { BobbinContext } from "#lib/api/client.ts"; import { resolveMiniDoc } from "#lib/api/identity.ts"; import type { Fetch } from "#lib/fetch.ts"; interface Image { src: string; alt: string; } interface Base { id: string; href: string; at: string; } interface Author { name: string; handle?: string; did?: string; } export interface BlogUpdate extends Base { kind: "blog"; title: string; authors: Author[]; summary?: string; image?: Image; } export interface ChangelogUpdate extends Base { kind: "changelog"; text: string; } export interface AnnouncementUpdate extends Base { kind: "announcement"; title: string; /** Paragraphs; `backticks` set a word in mono. */ body: string[]; } export type Update = BlogUpdate | ChangelogUpdate | AnnouncementUpdate; export const KIND_LABEL: Record, string> = { changelog: "Changelog", announcement: "Announcement" }; const DATE = new Intl.DateTimeFormat("en-US", { month: "short", day: "2-digit", year: "numeric", timeZone: "UTC" }); /** Jul 13, 2026. */ export const date = (at: string) => DATE.format(new Date(at)); /** How many updates the section shows. */ const LIVE_COUNT = 10; const PROFILE_ORIGIN = "https://tangled.org"; const handleOf = (profileUrl: string | undefined) => { const url = URL.parse(profileUrl ?? ""); return url?.origin === PROFILE_ORIGIN ? url.pathname.slice(1) || undefined : undefined; }; export const resolveAuthorDids = (ctx: BobbinContext, updates: Update[]): void => { for (const update of updates) { if (update.kind !== "blog") continue; for (const author of update.authors) { if (!author.handle) continue; void resolveMiniDoc(ctx, author.handle).then( (doc) => (author.did = doc.did), () => {} ); } } }; export const fromBlogFeed = (items: BlogFeedItem[], count = LIVE_COUNT): Update[] => items.slice(0, count).map((item) => ({ kind: "blog", id: item.url, href: item.url, at: item.date_published, title: item.title, authors: (item.authors ?? []).map((author) => ({ name: author.name, handle: handleOf(author.url) })), summary: item.summary || undefined, image: item.image ? { src: item.image, alt: "" } : undefined })); export async function loadLiveUpdates(fetch: Fetch, timeout = 2500): Promise { const items = await loadBlogFeed(fetch, timeout); return items && fromBlogFeed(items); }