import type { Cache } from "../core/cache.ts"; import type { Integration, IntegrationContext } from "../contracts/index.ts"; export interface GoogleFontsOptions { families: string[]; route?: string; cache?: Cache; display?: "auto" | "block" | "swap" | "fallback" | "optional"; } function memoryCache(): Cache { const entries = new Map(); return { async get(key) { return entries.get(key); }, async put(key, value) { entries.set(key, value); }, async delete(key) { entries.delete(key); }, }; } function text(value: Uint8Array): string { return new TextDecoder().decode(value); } function googleUrl(options: GoogleFontsOptions): string { const family = options.families.map((value) => `family=${encodeURIComponent(value.replace(/\s+/g, "+"))}`).join("&"); return `https://fonts.googleapis.com/css2?${family}&display=${options.display ?? "swap"}`; } /** Serves Google Fonts through project routes and caches both CSS and font files. */ export function googleFonts(options: GoogleFontsOptions): Integration { const route = options.route ?? "/_muddle/fonts.css"; const proxyPrefix = `${route.replace(/\.css$/, "")}/`; const cache = options.cache ?? memoryCache(); return { name: "google-fonts", setup(context: IntegrationContext) { context.route("GET", route, async () => { const key = `css:${googleUrl(options)}`; let css = await cache.get(key); if (!css) { const response = await fetch(googleUrl(options)); if (!response.ok) return new Response("", { status: 502 }); css = new Uint8Array(await response.arrayBuffer()); await cache.put(key, css, { ttl: 86400 }); } const proxied = text(css).replace(/url\((https:\/\/[^)]+)\)/g, (_match, url: string) => `url(${proxyPrefix}${encodeURIComponent(url)})`); return new Response(proxied, { headers: { "content-type": "text/css; charset=utf-8", "cache-control": "public, max-age=86400" } }); }); context.route("GET", `${proxyPrefix}*`, async (request) => { const encoded = new URL(request.url).pathname.slice(proxyPrefix.length); const url = decodeURIComponent(encoded); if (!url.startsWith("https://fonts.gstatic.com/")) return new Response("Not Found", { status: 404 }); const key = `font:${url}`; let font = await cache.get(key); if (!font) { const response = await fetch(url); if (!response.ok) return new Response("", { status: 502 }); font = new Uint8Array(await response.arrayBuffer()); await cache.put(key, font, { ttl: 31536000 }); } return new Response(font, { headers: { "content-type": "font/woff2", "cache-control": "public, max-age=31536000, immutable" } }); }); }, }; }