Skip to content

Instantly share code, notes, and snippets.

@sergiodxa
Last active June 12, 2024 04:04
Show Gist options
  • Select an option

  • Save sergiodxa/63770bbe411e2539652201454ccd1b99 to your computer and use it in GitHub Desktop.

Select an option

Save sergiodxa/63770bbe411e2539652201454ccd1b99 to your computer and use it in GitHub Desktop.
A pair of Cloudflare Workers that can use Satori to render JSX to SVG and then Resvg to convert it to PNG
import type { SatoriOptions } from "satori";
type FontNamedWeight = "regular" | "medium" | "bold" | "semibold";
function fontNameToWeight(name: FontNamedWeight) {
if (name === "regular") return 400;
if (name === "medium") return 500;
if (name === "semibold") return 600;
if (name === "bold") return 700;
}
export async function getFonts(
fonts: R2Bucket
): Promise<SatoriOptions["fonts"]> {
let font1: Promise<SatoriOptions["fonts"]> = Promise.all(
["regular" as const, "medium" as const, "bold" as const].map(
async (name) => {
return {
name: "Font 1",
data: await readFont(fonts, `font1-${name}.ttf`),
weight: fontNameToWeight(name),
style: "normal",
};
}
)
);
let font2: Promise<SatoriOptions["fonts"]> = Promise.all(
["regular" as const, "medium" as const, "semibold" as const].map(
async (name) => {
return {
name: "Font 2",
data: await readFont(fonts, `font2-${name}.ttf`),
weight: fontNameToWeight(name),
style: "normal",
};
}
)
);
let satoriFonts = await Promise.all([font1, font2]);
return satoriFonts.flat(1);
}
// Since you can't use the file system in Cloudflare, you can use an R2 bucket to simulate it
async function readFont(bucket: R2Bucket, key: string) {
let file = await bucket.get(key);
if (!file) throw new Error(`Could not find font ${key}`);
return await file.arrayBuffer();
}
import * as React from "react";
import satori from "satori";
import { ZodError } from "zod";
import { Card } from "./card";
import { getFonts } from "./get-fonts";
import { Schema } from "./schema";
export default {
async fetch(request, { fonts }) {
if (request.method.toUpperCase() !== "POST") {
return new Response(null, { status: 405 });
}
try {
let fontFiles = getFonts(fonts);
let { props, options } = await Schema.promise().parse(request.json());
let svg = await satori(<Card {...props} />, {
...options,
fonts: await fontFiles,
});
return new Response(svg, {
headers: { "Content-Type": "image/svg+xml" },
});
} catch (exception) {
if (exception instanceof ZodError) {
console.log(exception);
return new Response(
exception.issues.map((issue) => issue.message).join("\n"),
{ status: 400 }
);
}
if (exception instanceof Error) {
console.log(exception);
return new Response(exception.message, { status: 400 });
}
return new Response("Unknown error", { status: 500 });
}
},
} satisfies ExportedHandler<{ fonts: R2Bucket }>;
import { Resvg, initWasm } from "@resvg/resvg-wasm";
import { cacheHeader } from "pretty-cache-header";
// Local copy of https://github.com/yisibl/resvg-js/blob/main/wasm/index_bg.wasm
import resvgWasm from "./index_bg.wasm";
let initialisedWasm = false; // Needed in development where you should only initialize the WASM lib once
export default {
async fetch(request) {
if (request.method.toUpperCase() !== "POST") {
return new Response(null, { status: 405 });
}
if (!initialisedWasm) {
try {
await initWasm(resvgWasm);
initialisedWasm = true;
} catch (error) {
console.error(error);
}
}
let svg = await request.text();
let png = new Resvg(svg).render().asPng();
return new Response(png, {
headers: {
"Content-Type": "image/png",
"Cache-Control": cacheHeader({
sMaxage: "1month",
public: true,
}),
},
});
},
} satisfies ExportedHandler<{}>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment