UNPKG

@kitn.ai/chat

Version:

Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.

88 lines (75 loc) 3.43 kB
// src/primitives/link-preview.ts // The optional, app-supplied bare-URL → metadata hook for <kc-link-preview>, plus the // `link` card's data type. The card stays PURE: it renders from supplied metadata // and never touches the network. CORS forbids reading cross-origin HTML in the // browser, so there is intentionally NO built-in network implementation here — an // app opts in with `configureLinkPreview({ fetchMetadata })` pointing at its OWN // backend/proxy. See docs/superpowers/specs/2026-06-13-kc-link-embed-cards-design.md. import type { CardEnvelope } from './card-contract'; /** Rich link / Open-Graph preview payload. The card renders from this; it never fetches. */ export interface LinkPreviewData { /** Canonical destination; opened via the contract `open` verb. */ url: string; /** og:title — falls back to the domain. */ title?: string; /** og:description — clamped to 3 lines. */ description?: string; /** og:image — degrades gracefully when missing/broken. */ image?: string; /** Alt for the preview image (defaults to title / decorative). */ imageAlt?: string; /** Site favicon. */ favicon?: string; /** Display domain; derived from `url` when omitted. */ domain?: string; /** og:site_name; preferred over `domain` when present. */ siteName?: string; } /** The full envelope an agent/server emits for a link preview card. */ export type LinkPreviewEnvelope = CardEnvelope<'link', LinkPreviewData>; /** The `type` discriminator for link preview cards. */ export const LINK_PREVIEW_TYPE = 'link' as const; /** App-supplied resolver: a bare URL → (partial) OG metadata. Usually hits YOUR backend. */ export type LinkMetadataFetcher = (url: string) => Promise<Partial<LinkPreviewData>>; let fetcher: LinkMetadataFetcher | undefined; /** * App opt-in: supply a function (usually hitting YOUR backend/proxy) that resolves * a bare URL to OG metadata. CORS forbids reading cross-origin HTML in the browser, * so there is intentionally NO default network implementation. */ export function configureLinkPreview(opts: { fetchMetadata: LinkMetadataFetcher }): void { fetcher = opts.fetchMetadata; } /** True when an app has registered a fetcher (the bare-URL path is available). */ export function hasLinkPreviewFetcher(): boolean { return fetcher !== undefined; } /** * Used by LinkPreview ONLY when the envelope lacks metadata AND a fetcher is set. * Returns the merged metadata or throws (card shows its fallback/error state). */ export async function resolveLinkMetadata(url: string): Promise<Partial<LinkPreviewData>> { if (!fetcher) throw new Error('No link-preview fetcher configured'); return fetcher(url); } /** Test-only: clear the configured fetcher so tests stay isolated. */ export function __resetLinkPreviewForTests(): void { fetcher = undefined; } /** Derive a clean display domain from a URL (strips a leading `www.`). `undefined` if unparseable. */ export function deriveDomain(url: string): string | undefined { try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return undefined; } } const RENDERABLE_SCHEMES = ['http:', 'https:']; /** True when `url` is a syntactically valid http(s) URL (the only renderable link schemes). */ export function isRenderableLink(url: string): boolean { try { return RENDERABLE_SCHEMES.includes(new URL(url).protocol); } catch { return false; } }