generate-og-images.mts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import { readFile, writeFile, mkdir } from "node:fs/promises";
  2. import { join, dirname } from "node:path";
  3. import { fileURLToPath } from "node:url";
  4. import satori from "satori";
  5. import { Resvg } from "@resvg/resvg-js";
  6. const __dirname = dirname(fileURLToPath(import.meta.url));
  7. const ROOT = join(__dirname, "..");
  8. const FONTS_DIR = join(ROOT, "apps/web/public");
  9. const WIDTH = 1200;
  10. const HEIGHT = 630;
  11. type Framework = "nextjs" | "vite" | "sveltekit";
  12. interface Example {
  13. dir: string;
  14. title: string;
  15. framework: Framework;
  16. }
  17. const EXAMPLES: Example[] = [
  18. { dir: "chat", title: "Chat Example", framework: "nextjs" },
  19. { dir: "dashboard", title: "Dashboard Example", framework: "nextjs" },
  20. { dir: "game-engine", title: "Game Engine Demo", framework: "nextjs" },
  21. { dir: "image", title: "Image Example", framework: "nextjs" },
  22. { dir: "no-ai", title: "No AI Example", framework: "nextjs" },
  23. { dir: "react-email", title: "React Email Example", framework: "nextjs" },
  24. { dir: "react-pdf", title: "React PDF Example", framework: "nextjs" },
  25. { dir: "react-three-fiber", title: "React Three Fiber Example", framework: "nextjs" },
  26. { dir: "remotion", title: "Remotion Example", framework: "nextjs" },
  27. { dir: "solid", title: "Solid Example", framework: "vite" },
  28. { dir: "svelte", title: "Svelte Example", framework: "vite" },
  29. { dir: "svelte-chat", title: "Svelte Chat Example", framework: "sveltekit" },
  30. { dir: "vue", title: "Vue Example", framework: "vite" },
  31. { dir: "vite-renderers", title: "Multi-Renderer Example", framework: "vite" },
  32. ];
  33. function getOutputPath(example: Example): string {
  34. const base = join(ROOT, "examples", example.dir);
  35. switch (example.framework) {
  36. case "nextjs":
  37. return join(base, "app", "opengraph-image.png");
  38. case "vite":
  39. return join(base, "public", "og-image.png");
  40. case "sveltekit":
  41. return join(base, "static", "og-image.png");
  42. }
  43. }
  44. function buildMarkup(title: string) {
  45. return {
  46. type: "div",
  47. props: {
  48. style: {
  49. width: "100%",
  50. height: "100%",
  51. display: "flex",
  52. flexDirection: "column",
  53. backgroundColor: "black",
  54. padding: "60px 80px",
  55. },
  56. children: [
  57. {
  58. type: "div",
  59. props: {
  60. style: {
  61. display: "flex",
  62. alignItems: "center",
  63. gap: "16px",
  64. },
  65. children: [
  66. {
  67. type: "svg",
  68. props: {
  69. width: 36,
  70. height: 36,
  71. viewBox: "0 0 16 16",
  72. fill: "white",
  73. children: {
  74. type: "path",
  75. props: {
  76. fillRule: "evenodd",
  77. clipRule: "evenodd",
  78. d: "M8 1L16 15H0L8 1Z",
  79. },
  80. },
  81. },
  82. },
  83. {
  84. type: "span",
  85. props: {
  86. style: {
  87. fontSize: 36,
  88. color: "#666",
  89. fontFamily: "Geist",
  90. fontWeight: 400,
  91. },
  92. children: "/",
  93. },
  94. },
  95. {
  96. type: "span",
  97. props: {
  98. style: {
  99. fontSize: 36,
  100. fontFamily: "Geist Pixel Square",
  101. fontWeight: 500,
  102. color: "white",
  103. },
  104. children: "json-render",
  105. },
  106. },
  107. ],
  108. },
  109. },
  110. {
  111. type: "div",
  112. props: {
  113. style: {
  114. display: "flex",
  115. flex: 1,
  116. flexDirection: "column",
  117. alignItems: "center",
  118. justifyContent: "center",
  119. },
  120. children: title.split("\n").map((line: string) => ({
  121. type: "span",
  122. props: {
  123. style: {
  124. fontSize: 72,
  125. fontFamily: "Geist",
  126. fontWeight: 400,
  127. color: "white",
  128. letterSpacing: "-0.02em",
  129. textAlign: "center",
  130. lineHeight: 1.2,
  131. },
  132. children: line,
  133. },
  134. })),
  135. },
  136. },
  137. ],
  138. },
  139. };
  140. }
  141. async function main() {
  142. const [geistRegular, geistPixelSquare] = await Promise.all([
  143. readFile(join(FONTS_DIR, "Geist-Regular.ttf")),
  144. readFile(join(FONTS_DIR, "GeistPixel-Square.ttf")),
  145. ]);
  146. const fonts = [
  147. { name: "Geist", data: geistRegular, weight: 400 as const, style: "normal" as const },
  148. { name: "Geist Pixel Square", data: geistPixelSquare, weight: 500 as const, style: "normal" as const },
  149. ];
  150. for (const example of EXAMPLES) {
  151. const svg = await satori(buildMarkup(example.title), {
  152. width: WIDTH,
  153. height: HEIGHT,
  154. fonts,
  155. });
  156. const resvg = new Resvg(svg, {
  157. fitTo: { mode: "width", value: WIDTH },
  158. });
  159. const png = resvg.render().asPng();
  160. const outPath = getOutputPath(example);
  161. await mkdir(dirname(outPath), { recursive: true });
  162. await writeFile(outPath, png);
  163. console.log(` ${example.dir} -> ${outPath.replace(ROOT + "/", "")}`);
  164. }
  165. console.log(`\nGenerated ${EXAMPLES.length} OG images.`);
  166. }
  167. main().catch((err) => {
  168. console.error(err);
  169. process.exit(1);
  170. });