{"id":21607977,"url":"https://github.com/yusufff/satori-node","last_synced_at":"2025-04-11T04:37:26.678Z","repository":{"id":178906218,"uuid":"658473051","full_name":"yusufff/satori-node","owner":"yusufff","description":"A node server running satori (@vercel/og)","archived":false,"fork":false,"pushed_at":"2023-07-05T11:54:40.000Z","size":4340,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-11T04:37:20.943Z","etag":null,"topics":["fastify","image-generation","node","satori","vercel-og"],"latest_commit_sha":null,"homepage":"https://satori-node.fly.dev/og?text=Hello","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yusufff.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-06-25T21:00:45.000Z","updated_at":"2025-01-08T04:20:49.000Z","dependencies_parsed_at":null,"dependency_job_id":"3f358382-1614-4774-b458-1ba87e38a922","html_url":"https://github.com/yusufff/satori-node","commit_stats":null,"previous_names":["yusufff/satori-node"],"tags_count":0,"template":true,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusufff%2Fsatori-node","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusufff%2Fsatori-node/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusufff%2Fsatori-node/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusufff%2Fsatori-node/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yusufff","download_url":"https://codeload.github.com/yusufff/satori-node/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248345256,"owners_count":21088231,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fastify","image-generation","node","satori","vercel-og"],"created_at":"2024-11-24T20:35:33.660Z","updated_at":"2025-04-11T04:37:26.667Z","avatar_url":"https://github.com/yusufff.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# Satori Node\n\n![Satori on Node](https://satori-node.fly.dev/og?text=Running%20Satori%20on%20Node)\n\nThis is an example node project running [Satori](https://github.com/vercel/satori) on a Node server with [Fastify](https://www.fastify.io/)\n\n## Why?\n\nVercel only supports [Edge Runtime](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation#limits) for its OG library.\n\n[Satori](https://github.com/vercel/satori) gives you only the building block for generating dynamic images on the fly. It only converts HTML and CSS to SVG.\n\nThis repo creates SVG from HTML and CSS using [Satori](https://github.com/vercel/satori), then converts that SVG to PNG using [resvg](https://github.com/RazrFalcon/resvg), and then streams that PNG using [Fastify](https://www.fastify.io/)\n\n## Development\n\n```sg\n# Clone the repo\ngit clone https://github.com/yusufff/satori-node\n\n# Install\nyarn install\n\n# Run\nyarn dev\n```\n\nThis will start the development server.\n\nGo to `http://localhost:3000/og?text=Hello` to see the image.\n\nThe source file for the generated image is located at `src/routes/og/index.tsx`\n\n## Production\n\n```sg\nyarn build:prod\nyarn start:prod\n```\n\nThis will generate and start a production build.\n\n## Examples\n\nYou can try out the Vercel's [Open Graph (OG) Image Examples](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation/og-image-examples)\n\n### Basic server cache for the generated images\n\n```tsx\nimport { renderToImage } from \"./render-to-image\";\nimport type { FastifyPluginAsync } from \"fastify\";\nimport React from \"react\";\nimport { Readable } from \"stream\";\n\n// Init a cache map\nconst cache = new Map\u003c\n  string,\n  {\n    image: Buffer;\n    contentType: string;\n  }\n\u003e();\n\nconst og: FastifyPluginAsync = async (fastify, _opts): Promise\u003cvoid\u003e =\u003e {\n  fastify.get\u003c{ Querystring: { text?: string } }\u003e(\n    \"/\",\n    async (_request, reply) =\u003e {\n      const text = _request.query.text;\n\n      if (!text) {\n        return reply.code(400).send({ error: \"Missing text query parameter\" });\n      }\n\n      // Return the cached image if it exists\n      const cacheHit = cache.get(_request.url);\n      if (cacheHit) {\n        const stream = new Readable({\n          read() {\n            this.push(cacheHit.image);\n            this.push(null);\n          },\n        });\n\n        reply.type(cacheHit.contentType);\n        return reply.send(stream);\n      }\n\n      try {\n        const imageRes = await renderToImage(\n          \u003cdiv\n            style={{\n              fontSize: 100,\n              background: \"white\",\n              width: \"100%\",\n              height: \"100%\",\n              display: \"flex\",\n              textAlign: \"center\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n            }}\n          \u003e\n            {text}\n          \u003c/div\u003e,\n          {\n            width: 1200,\n            height: 600,\n            debug: false,\n          }\n        );\n\n        // Cache the image\n        cache.set(_request.url, {\n          image: imageRes.image,\n          contentType: imageRes.contentType,\n        });\n\n        const stream = new Readable({\n          read() {\n            this.push(imageRes.image);\n            this.push(null);\n          },\n        });\n\n        reply.type(imageRes.contentType);\n        return reply.send(stream);\n      } catch (err) {\n        console.log(err);\n        return reply.code(500).send({ error: \"Internal server error\" });\n      }\n    }\n  );\n};\n\nexport default og;\n```\n\n### Google Font Preview\n\n```tsx\nimport { renderToImage } from \"../og/render-to-image\";\nimport type { FastifyPluginAsync } from \"fastify\";\nimport fetch from \"node-fetch\";\nimport React from \"react\";\nimport { Font } from \"satori\";\nimport { Readable } from \"stream\";\n\nconst FontPreview: FastifyPluginAsync = async (\n  fastify,\n  _opts\n): Promise\u003cvoid\u003e =\u003e {\n  fastify.get\u003c{\n    Querystring: { font?: string; color?: string; size?: string };\n  }\u003e(\"/\", async (_request, reply) =\u003e {\n    const { font, color, size } = _request.query;\n\n    const fontSize = size ? parseInt(size) : 100;\n\n    if (!font) {\n      return reply.code(400).send({ error: \"Missing font query parameter\" });\n    }\n\n    try {\n      const url = new URL(\"https://www.googleapis.com/webfonts/v1/webfonts\");\n      url.searchParams.set(\"family\", font);\n      url.searchParams.set(\"key\", process.env.GCLOUD_API_KEY ?? \"\");\n      const fontReq = await fetch(url.toString());\n      const fontRes = (await fontReq.json()) as google.fonts.WebfontList \u0026 {\n        error: { code: number };\n      };\n\n      if (fontRes?.error) {\n        return reply.code(400).send({ error: \"Invalid font name\" });\n      }\n\n      const fontFamily = fontRes.items[0].family;\n      const fontFile = fontRes.items[0].files.regular;\n      const fontBuffer = await fetch(fontFile).then((res) =\u003e res.buffer());\n\n      const fonts: Font[] = [\n        {\n          name: fontFamily,\n          data: fontBuffer,\n          weight: 400,\n          style: \"normal\",\n        },\n      ];\n\n      const imageRes = await renderToImage(\n        \u003cdiv\n          style={{\n            fontSize: fontSize,\n            width: \"100%\",\n            height: \"100%\",\n            display: \"flex\",\n            textAlign: \"left\",\n            color: color ?? \"black\",\n            paddingBottom: fontSize * 0.2,\n          }}\n        \u003e\n          {fontFamily}\n        \u003c/div\u003e,\n        {\n          height: fontSize + fontSize * 0.2,\n          debug: false,\n          fonts,\n        }\n      );\n\n      const stream = new Readable({\n        read() {\n          this.push(imageRes.image);\n          this.push(null);\n        },\n      });\n\n      reply.type(imageRes.contentType);\n      return reply.send(stream);\n    } catch (err) {\n      console.log(err);\n      return reply.code(500).send({ error: \"Internal server error\" });\n    }\n  });\n};\n\nexport default FontPreview;\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyusufff%2Fsatori-node","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyusufff%2Fsatori-node","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyusufff%2Fsatori-node/lists"}