-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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