generate-og-images.mts 5.3 KB

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