import { defineEventHandler, getHeader, getMethod, getQuery, getRequestURL, type H3Event, } from "h3"; import { getAppName } from "./app-name.js"; import { resolveBuiltInAuthMarketing, resolveBuiltInAuthMarketingByName, } from "./auth-marketing.js"; import { OG_ARABIC_FONT_FAMILY, OG_FONT_FAMILY, resolveOgFontFiles, } from "./og-fonts.js"; export interface AgentNativeOgImageInput { appName?: string | null; title?: string | null; accentText?: string | null; } export const AGENT_NATIVE_OG_IMAGE_WIDTH = 1200; export const AGENT_NATIVE_OG_IMAGE_HEIGHT = 630; export const AGENT_NATIVE_OG_IMAGE_CACHE_CONTROL = "public, max-age=60, stale-while-revalidate=604800, stale-if-error=3600"; export const AGENT_NATIVE_OG_IMAGE_NETLIFY_CACHE_CONTROL = "public, durable, max-age=60, stale-while-revalidate=604800, stale-if-error=3600"; const WIDTH = AGENT_NATIVE_OG_IMAGE_WIDTH; const HEIGHT = AGENT_NATIVE_OG_IMAGE_HEIGHT; const BRAND_BLUE = "#00B5FF"; const BRAND_MINT = "#48FFE4"; const BG = "#000000"; const FG = "#f5f5f5"; const GRID_SIZE = 48; const DEFAULT_FONT_FAMILY = `${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`; const ARABIC_FONT_FAMILY = `${OG_ARABIC_FONT_FAMILY}, ${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`; const DEFAULT_ACCENT_TEXT = "100% free and open source"; const LOGO_MARK = ` `; function escapeSvg(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function cleanText(value: string | null | undefined): string { return String(value ?? "") .replace(/\s+/g, " ") .trim(); } function titleCase(value: string): string { return value .split(/[\s._-]+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) .join(" "); } function titleFromAppName(appName: string): string { if (appName) return appName; const basePath = process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || ""; const slug = basePath.split("/").filter(Boolean)[0] || ""; return titleCase(slug) || "Agent-Native"; } interface WrappedText { lines: string[]; truncated: boolean; } interface TitleLayout { lines: string[]; fontSize: number; lineHeight: number; } function estimateTextWidth(value: string, fontSize: number): number { let units = 0; for (const char of value) { if (char === " ") { units += 0.28; } else if (/[MW@#%&]/.test(char)) { units += 0.86; } else if (/[A-Z]/.test(char)) { units += 0.64; } else if (/[ilI.,:;|!']/u.test(char)) { units += 0.26; } else if (/[0-9]/.test(char)) { units += 0.56; } else { units += 0.54; } } return units * fontSize; } function containsArabicText(value: string): boolean { return /[\u0600-\u06ff\u0750-\u077f\u0870-\u089f\ufb50-\ufdff\ufe70-\ufeff]/u.test( value, ); } function fontFamilyForText(value: string): string { return containsArabicText(value) ? ARABIC_FONT_FAMILY : DEFAULT_FONT_FAMILY; } function trimTextToWidth( value: string, fontSize: number, maxWidth: number, ): string { const ellipsis = "..."; let trimmed = value.trim(); while ( trimmed.length > 0 && estimateTextWidth(`${trimmed}${ellipsis}`, fontSize) > maxWidth ) { trimmed = trimmed.slice(0, -1).trimEnd(); } return trimmed ? `${trimmed}${ellipsis}` : ellipsis; } function wrapTextToWidth( value: string, fontSize: number, maxWidth: number, maxLines: number, ): WrappedText { const words = value.split(/\s+/).filter(Boolean); const lines: string[] = []; let current = ""; let truncated = false; for (const word of words) { const next = current ? `${current} ${word}` : word; if (estimateTextWidth(next, fontSize) <= maxWidth) { current = next; continue; } if (!current) { lines.push(trimTextToWidth(word, fontSize, maxWidth)); truncated = true; current = ""; } else { lines.push(current); current = word; } if (lines.length === maxLines) { truncated = true; break; } } if (current && lines.length < maxLines) lines.push(current); const usedWordCount = lines.join(" ").split(/\s+/).filter(Boolean).length; if (usedWordCount < words.length && lines.length > 0) { lines[lines.length - 1] = trimTextToWidth( lines[lines.length - 1], fontSize, maxWidth, ); truncated = true; } return { lines: lines.length ? lines : [trimTextToWidth(value, fontSize, maxWidth)], truncated, }; } function getTitleLayout(title: string): TitleLayout { const maxTitleWidth = 900; if (estimateTextWidth(title, 88) <= maxTitleWidth) { return { lines: [title], fontSize: 88, lineHeight: 96, }; } for (const fontSize of [76, 70, 64, 58, 52]) { const wrapped = wrapTextToWidth(title, fontSize, maxTitleWidth, 2); if (!wrapped.truncated) { const lineHeight = Math.round(fontSize * 1.1); return { lines: wrapped.lines, fontSize, lineHeight, }; } } const fallbackFontSize = 52; const wrapped = wrapTextToWidth(title, fallbackFontSize, maxTitleWidth, 2); return { lines: wrapped.lines, fontSize: fallbackFontSize, lineHeight: 60, }; } function textBlock({ lines, x, y, fontSize, lineHeight, weight, fill, anchor = "start", direction, fontFamily = DEFAULT_FONT_FAMILY, }: { lines: string[]; x: number; y: number; fontSize: number; lineHeight: number; weight: number; fill: string; anchor?: "start" | "middle" | "end"; direction?: "ltr" | "rtl"; fontFamily?: string; }): string { const directionAttrs = direction ? ` direction="${direction}" unicode-bidi="plaintext"` : ""; return `${lines .map( (line, index) => `${escapeSvg(line)}`, ) .join("")}`; } export function resolveAgentNativeOgImageAppName(event?: H3Event): string { const explicitAppName = cleanText(process.env.APP_NAME); if (explicitAppName) { return ( resolveBuiltInAuthMarketingByName(explicitAppName)?.appName ?? explicitAppName ); } const requestHost = event ? (getHeader(event, "x-forwarded-host") ?? getHeader(event, "host")) : undefined; const requestPath = event ? getRequestURL(event).pathname : undefined; const builtInAppName = resolveBuiltInAuthMarketing({ requestHost, requestPath, })?.appName; if (builtInAppName) return builtInAppName; const appName = getAppName(); if (appName) { return resolveBuiltInAuthMarketingByName(appName)?.appName ?? appName; } return "Agent-Native"; } function queryStringValue( value: unknown, maxLength: number, ): string | undefined { if (typeof value !== "string") return undefined; const clean = cleanText(value).slice(0, maxLength); return clean || undefined; } function pngBody(bytes: Uint8Array): ArrayBuffer { const body = new ArrayBuffer(bytes.byteLength); new Uint8Array(body).set(bytes); return body; } function textByteLength(value: string): number { return new TextEncoder().encode(value).byteLength; } export function isResvgRuntimeUnavailableError(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error ?? ""); return ( /@resvg\/resvg-js|resvgjs\.[\w-]+\.node|native binding/i.test(message) && // "no such module" is workerd's wording when the package is externalized // out of the Cloudflare worker bundle. /cannot find|no such module|err_module_not_found|dlopen|invalid elf|wrong architecture|not a valid win32|native binding/i.test( message, ) ); } export function renderAgentNativeOgImageSvg( input: AgentNativeOgImageInput = {}, ): string { const appName = cleanText(input.appName) || resolveAgentNativeOgImageAppName(); const title = cleanText(input.title) || titleFromAppName(appName); const accentText = cleanText(input.accentText) || DEFAULT_ACCENT_TEXT; const titleLayout = getTitleLayout(title); const titleIsRtl = containsArabicText(title); const textX = titleIsRtl ? WIDTH - 80 : 80; const accentX = titleIsRtl ? WIDTH - 84 : 84; const textAnchor = titleIsRtl ? "end" : "start"; const titleY = titleLayout.lines.length > 1 ? 288 : 330; const accentY = titleY + titleLayout.lineHeight * (titleLayout.lines.length - 1) + 70; return ` ${escapeSvg(title)} - Agent-Native preview ${LOGO_MARK} ${textBlock({ lines: titleLayout.lines, x: textX, y: titleY, fontSize: titleLayout.fontSize, lineHeight: titleLayout.lineHeight, // resvg's fontdb maps font-weight 850 to the Regular face (only 400/700 // exist for Liberation Sans); 800 resolves to Bold, the heaviest face we // bundle, which is the intended look for the display title. weight: 800, fill: FG, anchor: textAnchor, direction: titleIsRtl ? "rtl" : undefined, fontFamily: fontFamilyForText(title), })} ${textBlock({ lines: [accentText], x: accentX, y: accentY, fontSize: 34, lineHeight: 40, weight: 800, fill: BRAND_BLUE, anchor: textAnchor, fontFamily: fontFamilyForText(accentText), })} `; } export async function renderAgentNativeOgImagePng( input: AgentNativeOgImageInput = {}, ): Promise { const overridePackage = typeof process !== "undefined" ? process.env.AGENT_NATIVE_RESVG_PACKAGE : undefined; const resvgPackage = overridePackage || "@resvg/resvg-js"; const { Resvg } = await import(/* @vite-ignore */ resvgPackage); // Feed resvg the embedded Liberation Sans font explicitly. System fonts can't // be relied on: Linux serverless runtimes (Netlify/Lambda) ship neither Arial // nor Inter, so without a bundled font every `` rendered blank. const fontFiles = resolveOgFontFiles(); const hasBundledFonts = Boolean(fontFiles?.length); const image = new Resvg(renderAgentNativeOgImageSvg(input), { fitTo: { mode: "width", value: WIDTH }, font: { loadSystemFonts: !hasBundledFonts, ...(hasBundledFonts ? { fontFiles } : {}), defaultFontFamily: OG_FONT_FAMILY, serifFamily: OG_FONT_FAMILY, sansSerifFamily: OG_FONT_FAMILY, }, }).render(); return image.asPng(); } export function agentNativeOgImageResponseHeaders( byteLength?: number, contentType = "image/png", ): Record { const headers: Record = { "Content-Type": contentType, "Cache-Control": AGENT_NATIVE_OG_IMAGE_CACHE_CONTROL, "CDN-Cache-Control": AGENT_NATIVE_OG_IMAGE_CACHE_CONTROL, "Netlify-CDN-Cache-Control": AGENT_NATIVE_OG_IMAGE_NETLIFY_CACHE_CONTROL, "Cross-Origin-Resource-Policy": "cross-origin", }; if (typeof byteLength === "number") { headers["Content-Length"] = String(byteLength); } return headers; } export function createAgentNativeOgImageHandler( options: AgentNativeOgImageInput = {}, ) { return defineEventHandler(async (event) => { if (getMethod(event) === "HEAD") { return new Response(null, { headers: agentNativeOgImageResponseHeaders(), }); } const query = getQuery(event); const appName = cleanText(options.appName) || resolveAgentNativeOgImageAppName(event); const input = { ...options, appName, title: cleanText(options.title) || queryStringValue(query.title, 140), accentText: cleanText(options.accentText) || queryStringValue(query.accentText, 80), }; let png: Uint8Array; try { png = await renderAgentNativeOgImagePng(input); } catch (error) { if (!isResvgRuntimeUnavailableError(error)) throw error; const svg = renderAgentNativeOgImageSvg(input); return new Response(svg, { headers: agentNativeOgImageResponseHeaders( textByteLength(svg), "image/svg+xml; charset=utf-8", ), }); } return new Response(pngBody(png), { headers: agentNativeOgImageResponseHeaders(png.byteLength), }); }); }