muddle/integrations/fonts.ts
2026-10-23 08:53:07 -04:00

65 lines
2.6 KiB
TypeScript

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<string, Uint8Array>();
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" } });
});
},
};
}