diff --git a/api/app.ts b/api/app.ts index 5ffb226..0e08808 100644 --- a/api/app.ts +++ b/api/app.ts @@ -395,18 +395,20 @@ export function createApp(config: AppConfig = {}) { // Escape `<` so `` in report markdown can't break out. const payload = JSON.stringify(diagnostic).replace(/window.__PUBLIC_DIAGNOSTIC__=${payload};`; - // Replace generic title then inject SEO tags + payload before . - const htmlWithTitle = html.replace( - /[^<]*<\/title>/, - `<title>${pageTitle}`, - ); - const base = htmlWithTitle.includes("") ? htmlWithTitle : html; - const withPayload = base.includes("</head>") - ? base.replace( + // Strip the static SEO tags from index.html so crawlers (which + // pick the first occurrence) don't read the generic defaults + // instead of our dynamic ones. + const stripped = html + .replace(/<title>[^<]*<\/title>\s*/i, "") + .replace(/<meta\s+name=["']description["'][^>]*\/?>\s*/gi, "") + .replace(/<meta\s+property=["']og:[^"']+["'][^>]*\/?>\s*/gi, "") + .replace(/<meta\s+name=["']twitter:[^"']+["'][^>]*\/?>\s*/gi, ""); + const withPayload = stripped.includes("</head>") + ? stripped.replace( "</head>", `\t\t${seoTags}\n\t\t${injected}\n\t</head>`, ) - : `${injected}${base}`; + : `${injected}${stripped}`; return new Response(withPayload, { headers: { "content-type": "text/html; charset=utf-8", diff --git a/api/assets/Inter-Bold.ttf b/api/assets/Inter-Bold.ttf deleted file mode 100644 index 15e908f..0000000 Binary files a/api/assets/Inter-Bold.ttf and /dev/null differ diff --git a/api/assets/Inter-Regular.ttf b/api/assets/Inter-Regular.ttf deleted file mode 100644 index c544be4..0000000 Binary files a/api/assets/Inter-Regular.ttf and /dev/null differ diff --git a/api/lib/og-image.tsx b/api/lib/og-image.tsx index 1bfdde5..6ba337e 100644 --- a/api/lib/og-image.tsx +++ b/api/lib/og-image.tsx @@ -1,36 +1,43 @@ -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; import { initWasm, Resvg } from "@resvg/resvg-wasm"; import satori from "satori"; import type { Diagnostic } from "../types/diagnostic.ts"; // ── Singletons initialised once per process ────────────────────────────────── +// Fetched from CDN once and held in memory. Works in both Bun (local dev) and +// Cloudflare Workers (production) — no filesystem required. -let resvgReady = false; +const RESVG_WASM_URL = + "https://unpkg.com/@resvg/resvg-wasm@2.6.2/index_bg.wasm"; +const INTER_REGULAR_URL = + "https://github.com/rsms/inter/raw/v4.0/extras/ttf/Inter-Regular.ttf"; +const INTER_BOLD_URL = + "https://github.com/rsms/inter/raw/v4.0/extras/ttf/Inter-Bold.ttf"; + +let initPromise: Promise<void> | null = null; let fontRegular: ArrayBuffer | null = null; let fontBold: ArrayBuffer | null = null; -async function init() { - if (resvgReady && fontRegular && fontBold) return; - - const assetsDir = join(import.meta.dir, "../assets"); - const [wasmBuf, reg, bold] = await Promise.all([ - readFile( - join( - import.meta.dir, - "../../node_modules/@resvg/resvg-wasm/index_bg.wasm", - ), - ), - readFile(join(assetsDir, "Inter-Regular.ttf")), - readFile(join(assetsDir, "Inter-Bold.ttf")), - ]); - - if (!resvgReady) { - await initWasm(wasmBuf.buffer as ArrayBuffer); - resvgReady = true; +async function fetchBytes(url: string): Promise<ArrayBuffer> { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch ${url}: ${res.status}`); } - fontRegular = reg.buffer as ArrayBuffer; - fontBold = bold.buffer as ArrayBuffer; + return res.arrayBuffer(); +} + +function init(): Promise<void> { + if (initPromise) return initPromise; + initPromise = (async () => { + const [wasm, reg, bold] = await Promise.all([ + fetchBytes(RESVG_WASM_URL), + fetchBytes(INTER_REGULAR_URL), + fetchBytes(INTER_BOLD_URL), + ]); + await initWasm(wasm); + fontRegular = reg; + fontBold = bold; + })(); + return initPromise; } // ── Helpers ───────────────────────────────────────────────────────────────────