muddle/integrations/fonts.ts
2026-10-23 09:12:49 -04:00

88 lines
2.7 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",
},
});
});
},
};
}