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 ``;
}
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),
});
});
}