import { DEFAULT_PUBLIC_BASE_URL, buildBundlePreview, humanizeType, maybeString, parseBundle, parseFrontmatter, } from "./share-utils.ts"; import { BASE_OG_IMAGE_HEIGHT, BASE_OG_IMAGE_WIDTH, getOgImageVariantConfig, type OgImageVariant, } from "./og-image-variants.ts"; export type OgImageModel = { title: string; fileName: string; fileType: string; description: string; category: string; tag: string; domain: string; }; export type OgTitleTier = "xl" | "lg" | "md" | "sm" | "xs"; export type OgImageLayout = { displayTitle: string; titleTier: OgTitleTier; titleFontSize: number; titleLineHeight: number; titleLines: string[]; showDescription: boolean; descriptionLines: string[]; }; type OgTextTierConfig = { fontSize: number; lineHeight: number; maxLines: number; charsPerLine: number; }; const MAX_DISPLAY_CHARS = 55; const MAX_DESCRIPTION_CHARS = 120; const DEFAULT_DOMAIN = DEFAULT_PUBLIC_BASE_URL.replace(/^https?:\/\//, ""); const TITLE_TIER_CONFIG: Record = { xl: { fontSize: 64, lineHeight: 68, maxLines: 1, charsPerLine: 18 }, lg: { fontSize: 50, lineHeight: 56, maxLines: 2, charsPerLine: 18 }, md: { fontSize: 40, lineHeight: 46, maxLines: 2, charsPerLine: 24 }, sm: { fontSize: 32, lineHeight: 38, maxLines: 2, charsPerLine: 30 }, xs: { fontSize: 26, lineHeight: 33, maxLines: 2, charsPerLine: 34 }, }; function escapeSvgText(value: unknown): string { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function normalizeText(value: unknown): string { return String(value ?? "").replace(/\s+/g, " ").trim(); } function titleFromFileName(fileName: string): string { return fileName .replace(/\.[a-z0-9]+$/i, "") .split(/[-_\s]+/) .filter(Boolean) .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`) .join(" "); } function humanizeTitle(value: string): string { const normalized = normalizeText(value); if (!normalized) return ""; if (/[A-Z]/.test(normalized)) return normalized; return normalized .split(/[-_\s]+/) .filter(Boolean) .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`) .join(" "); } function truncateAtWordBoundary(value: string, maxChars: number): string { if (value.length <= maxChars) return value; const truncated = value.slice(0, maxChars); const lastSpace = truncated.lastIndexOf(" "); if (lastSpace > maxChars * 0.6) { return `${truncated.slice(0, lastSpace)}...`; } return `${truncated}...`; } function splitTextIntoLines(text: string, maxCharsPerLine: number, maxLines: number): string[] { const normalized = normalizeText(text); if (!normalized) return []; const words = normalized.split(" "); const lines: string[] = []; let current = ""; for (const word of words) { const candidate = current ? `${current} ${word}` : word; if (candidate.length <= maxCharsPerLine) { current = candidate; continue; } if (current) { lines.push(current); if (lines.length === maxLines) { lines[lines.length - 1] = truncateAtWordBoundary(lines[lines.length - 1]!, maxCharsPerLine); return lines; } current = word; continue; } lines.push(truncateAtWordBoundary(word, maxCharsPerLine)); if (lines.length === maxLines) { return lines; } } if (current && lines.length < maxLines) { lines.push(current); } if (lines.length > maxLines) { return lines.slice(0, maxLines); } if (lines.length === maxLines && words.join(" ").length > lines.join(" ").length) { lines[lines.length - 1] = truncateAtWordBoundary(lines[lines.length - 1]!, maxCharsPerLine); } return lines; } function getTitleTier(length: number): OgTitleTier { if (length <= 10) return "xl"; if (length <= 20) return "lg"; if (length <= 35) return "md"; if (length <= 50) return "sm"; return "xs"; } function inferFileType(fileName: string, category: string): string { const extension = (fileName.split(".").pop() || "").toLowerCase(); if (extension === "md" && category === "command") return "COMMAND.md"; if (extension === "md") return "SKILL.md"; if (extension === "json") return "JSON"; if (extension === "toml") return "TOML"; if (extension === "yaml" || extension === "yml") return "YAML"; return extension ? extension.toUpperCase() : "FILE"; } function buildTagFromTrigger(trigger: string): string { const normalized = normalizeText(trigger); return normalized ? `trigger: ${normalized.toLowerCase()}` : ""; } function buildCategory(bundleType: string, previewTone: string): string { const typeLabel = humanizeType(bundleType).trim(); if (typeLabel) return typeLabel.toLowerCase(); return normalizeText(previewTone).toLowerCase() || "bundle"; } function buildDescription(options: { bundleDescription: string; frontmatterDescription: string; previewLabel: string; }): string { return truncateAtWordBoundary( normalizeText(options.bundleDescription) || normalizeText(options.frontmatterDescription) || normalizeText(options.previewLabel), MAX_DESCRIPTION_CHARS, ); } function buildRootOgInput(): OgImageModel { return { title: "Share OpenWork skills beautifully", fileName: "agent-creator.md", fileType: "SKILL.md", description: "Clean metadata-first social cards for shared OpenWork skills and bundles.", category: "share", tag: "openwork preview", domain: DEFAULT_DOMAIN, }; } function buildBundleOgInput({ rawJson }: { id: string; rawJson: string }): OgImageModel { const bundle = parseBundle(rawJson); const preview = buildBundlePreview(bundle); const { data } = parseFrontmatter(bundle.content); const bundleName = maybeString(data.name).trim() || bundle.name || titleFromFileName(preview.filename) || "OpenWork bundle"; const bundleDescription = maybeString(bundle.description).trim(); const frontmatterDescription = maybeString(data.description).trim(); const triggerTag = buildTagFromTrigger( maybeString(data.trigger).trim() || maybeString(bundle.trigger).trim(), ); const title = bundle.type === "workspace-profile" ? "Workspace Profile" : bundle.type === "skills-set" && bundle.skills.length > 1 ? `${bundle.skills.length} Shared Skills` : humanizeTitle(bundleName) || "OpenWork bundle"; const category = buildCategory(bundle.type, preview.tone); const tag = triggerTag || normalizeText(preview.label).toLowerCase() || `${category} bundle`; return { title, fileName: preview.filename, fileType: inferFileType(preview.filename, preview.tone), description: buildDescription({ bundleDescription, frontmatterDescription, previewLabel: preview.label, }), category, tag, domain: DEFAULT_DOMAIN, }; } export function computeOgImageLayout(model: OgImageModel): OgImageLayout { const displayTitle = truncateAtWordBoundary(humanizeTitle(model.title) || "OpenWork bundle", MAX_DISPLAY_CHARS); const titleTier = getTitleTier(displayTitle.length); const config = TITLE_TIER_CONFIG[titleTier]; const titleLines = splitTextIntoLines(displayTitle, config.charsPerLine, config.maxLines); const showDescription = Boolean(model.description) && (titleTier === "xl" || titleTier === "lg"); const descriptionLines = showDescription ? splitTextIntoLines(model.description, 42, 2) : []; return { displayTitle, titleTier, titleFontSize: config.fontSize, titleLineHeight: config.lineHeight, titleLines, showDescription, descriptionLines, }; } function renderOpenWorkLogo({ x, y, width, height }: { x: number; y: number; width: number; height: number }): string { return ` `; } function renderTitleBlock(model: OgImageModel): string { const layout = computeOgImageLayout(model); const cardX = 108; const cardY = 82; const titleWidth = 720; const titleX = cardX + 72; const descriptionLineHeight = 24; const blockHeight = layout.titleLines.length * layout.titleLineHeight + (layout.showDescription ? layout.descriptionLines.length * descriptionLineHeight + 22 : 0); let currentY = cardY + 242 - blockHeight / 2 + layout.titleFontSize; const titleMarkup = layout.titleLines .map((line, index) => { const node = `${escapeSvgText(line)}`; return node; }) .join(""); currentY += layout.titleLines.length * layout.titleLineHeight; const descriptionMarkup = layout.showDescription ? layout.descriptionLines .map((line, index) => { const y = currentY + 22 + index * descriptionLineHeight; return `${escapeSvgText(line)}`; }) .join("") : ""; return ` ${titleMarkup} ${descriptionMarkup} `; } function renderSkillCard(model: OgImageModel, variant: OgImageVariant): string { const variantConfig = getOgImageVariantConfig(variant); const cardX = 108; const cardY = 82; const cardWidth = 984; const cardHeight = 466; const badgeWidth = 132; const badgeX = cardX + cardWidth - 72 - badgeWidth; const badgeY = cardY + 44; return ` ${escapeSvgText(model.domain)} ${renderOpenWorkLogo({ x: cardX + 66, y: cardY + 34, width: 40, height: 34 })} ${escapeSvgText(model.fileType)} ${renderTitleBlock(model)} ${escapeSvgText(model.category.toUpperCase())} / ${escapeSvgText(model.tag)} `; } export function buildRootOgImageModel(): OgImageModel { return buildRootOgInput(); } export function buildBundleOgImageModel({ id, rawJson }: { id: string; rawJson: string }): OgImageModel { return buildBundleOgInput({ id, rawJson }); } export function renderRootOgImage(variant: OgImageVariant = "facebook"): string { return renderSkillCard(buildRootOgInput(), variant); } export function renderBundleOgImage({ id, rawJson, variant = "facebook", }: { id: string; rawJson: string; variant?: OgImageVariant; }): string { return renderSkillCard(buildBundleOgInput({ id, rawJson }), variant); }