@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
text/typescript
// 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;
}
}