This is an example node project running Satori on a Node server with Fastify
Vercel only supports Edge Runtime for its OG library.
Satori gives you only the building block for generating dynamic images on the fly. It only converts HTML and CSS to SVG.
This repo creates SVG from HTML and CSS using Satori, then converts that SVG to PNG using resvg, and then streams that PNG using Fastify
# Clone the repo
git clone https://github.com/yusufff/satori-node
# Install
yarn install
# Run
yarn dev
This will start the development server.
Go to http://localhost:3000/og?text=Hello to see the image.
The source file for the generated image is located at src/routes/og/index.tsx
yarn build:prod
yarn start:prod
This will generate and start a production build.
You can try out the Vercel's Open Graph (OG) Image Examples
import{renderToImage}from"./render-to-image";importtype{FastifyPluginAsync}from"fastify";importReactfrom"react";import{Readable}from"stream";// Init a cache mapconstcache=newMap<string,{image: Buffer;contentType: string;}>();constog: FastifyPluginAsync=async(fastify,_opts): Promise<void>=>{fastify.get<{Querystring: {text?: string}}>("/",async(_request,reply)=>{consttext=_request.query.text;if(!text){returnreply.code(400).send({error: "Missing text query parameter"});}// Return the cached image if it existsconstcacheHit=cache.get(_request.url);if(cacheHit){conststream=newReadable({read(){this.push(cacheHit.image);this.push(null);},});reply.type(cacheHit.contentType);returnreply.send(stream);}try{constimageRes=awaitrenderToImage(<divstyle={{fontSize: 100,background: "white",width: "100%",height: "100%",display: "flex",textAlign: "center",alignItems: "center",justifyContent: "center",}}>{text}</div>,{width: 1200,height: 600,debug: false,});// Cache the imagecache.set(_request.url,{image: imageRes.image,contentType: imageRes.contentType,});conststream=newReadable({read(){this.push(imageRes.image);this.push(null);},});reply.type(imageRes.contentType);returnreply.send(stream);}catch(err){console.log(err);returnreply.code(500).send({error: "Internal server error"});}});};exportdefaultog;import{renderToImage}from"../og/render-to-image";importtype{FastifyPluginAsync}from"fastify";importfetchfrom"node-fetch";importReactfrom"react";import{Font}from"satori";import{Readable}from"stream";constFontPreview: FastifyPluginAsync=async(fastify,_opts): Promise<void>=>{fastify.get<{Querystring: {font?: string;color?: string;size?: string};}>("/",async(_request,reply)=>{const{ font, color, size }=_request.query;constfontSize=size ? parseInt(size) : 100;if(!font){returnreply.code(400).send({error: "Missing font query parameter"});}try{consturl=newURL("https://www.googleapis.com/webfonts/v1/webfonts");url.searchParams.set("family",font);url.searchParams.set("key",process.env.GCLOUD_API_KEY??"");constfontReq=awaitfetch(url.toString());constfontRes=(awaitfontReq.json())asgoogle.fonts.WebfontList&{error: {code: number};};if(fontRes?.error){returnreply.code(400).send({error: "Invalid font name"});}constfontFamily=fontRes.items[0].family;constfontFile=fontRes.items[0].files.regular;constfontBuffer=awaitfetch(fontFile).then((res)=>res.buffer());constfonts: Font[]=[{name: fontFamily,data: fontBuffer,weight: 400,style: "normal",},];constimageRes=awaitrenderToImage(<divstyle={{fontSize: fontSize,width: "100%",height: "100%",display: "flex",textAlign: "left",color: color??"black",paddingBottom: fontSize*0.2,}}>{fontFamily}</div>,{height: fontSize+fontSize*0.2,debug: false,
fonts,});conststream=newReadable({read(){this.push(imageRes.image);this.push(null);},});reply.type(imageRes.contentType);returnreply.send(stream);}catch(err){console.log(err);returnreply.code(500).send({error: "Internal server error"});}});};exportdefaultFontPreview;