mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-02 03:54:41 +08:00
186 lines
5.4 KiB
TypeScript
186 lines
5.4 KiB
TypeScript
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import satori from "satori";
|
|
import { Resvg } from "@resvg/resvg-js";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(__dirname, "..");
|
|
const FONTS_DIR = join(ROOT, "apps/web/public");
|
|
|
|
const WIDTH = 1200;
|
|
const HEIGHT = 630;
|
|
|
|
type Framework = "nextjs" | "vite" | "sveltekit";
|
|
|
|
interface Example {
|
|
dir: string;
|
|
title: string;
|
|
framework: Framework;
|
|
}
|
|
|
|
const EXAMPLES: Example[] = [
|
|
{ dir: "chat", title: "Chat Example", framework: "nextjs" },
|
|
{ dir: "dashboard", title: "Dashboard Example", framework: "nextjs" },
|
|
{ dir: "game-engine", title: "Game Engine Demo", framework: "nextjs" },
|
|
{ dir: "image", title: "Image Example", framework: "nextjs" },
|
|
{ dir: "no-ai", title: "No AI Example", framework: "nextjs" },
|
|
{ dir: "react-email", title: "React Email Example", framework: "nextjs" },
|
|
{ dir: "react-pdf", title: "React PDF Example", framework: "nextjs" },
|
|
{ dir: "react-three-fiber", title: "React Three Fiber Example", framework: "nextjs" },
|
|
{ dir: "remotion", title: "Remotion Example", framework: "nextjs" },
|
|
{ dir: "solid", title: "Solid Example", framework: "vite" },
|
|
{ dir: "svelte", title: "Svelte Example", framework: "vite" },
|
|
{ dir: "svelte-chat", title: "Svelte Chat Example", framework: "sveltekit" },
|
|
{ dir: "vue", title: "Vue Example", framework: "vite" },
|
|
{ dir: "vite-renderers", title: "Multi-Renderer Example", framework: "vite" },
|
|
];
|
|
|
|
function getOutputPath(example: Example): string {
|
|
const base = join(ROOT, "examples", example.dir);
|
|
switch (example.framework) {
|
|
case "nextjs":
|
|
return join(base, "app", "opengraph-image.png");
|
|
case "vite":
|
|
return join(base, "public", "og-image.png");
|
|
case "sveltekit":
|
|
return join(base, "static", "og-image.png");
|
|
}
|
|
}
|
|
|
|
function buildMarkup(title: string) {
|
|
return {
|
|
type: "div",
|
|
props: {
|
|
style: {
|
|
width: "100%",
|
|
height: "100%",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
backgroundColor: "black",
|
|
padding: "60px 80px",
|
|
},
|
|
children: [
|
|
{
|
|
type: "div",
|
|
props: {
|
|
style: {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "16px",
|
|
},
|
|
children: [
|
|
{
|
|
type: "svg",
|
|
props: {
|
|
width: 36,
|
|
height: 36,
|
|
viewBox: "0 0 16 16",
|
|
fill: "white",
|
|
children: {
|
|
type: "path",
|
|
props: {
|
|
fillRule: "evenodd",
|
|
clipRule: "evenodd",
|
|
d: "M8 1L16 15H0L8 1Z",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "span",
|
|
props: {
|
|
style: {
|
|
fontSize: 36,
|
|
color: "#666",
|
|
fontFamily: "Geist",
|
|
fontWeight: 400,
|
|
},
|
|
children: "/",
|
|
},
|
|
},
|
|
{
|
|
type: "span",
|
|
props: {
|
|
style: {
|
|
fontSize: 36,
|
|
fontFamily: "Geist Pixel Square",
|
|
fontWeight: 500,
|
|
color: "white",
|
|
},
|
|
children: "json-render",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
type: "div",
|
|
props: {
|
|
style: {
|
|
display: "flex",
|
|
flex: 1,
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
children: title.split("\n").map((line: string) => ({
|
|
type: "span",
|
|
props: {
|
|
style: {
|
|
fontSize: 72,
|
|
fontFamily: "Geist",
|
|
fontWeight: 400,
|
|
color: "white",
|
|
letterSpacing: "-0.02em",
|
|
textAlign: "center",
|
|
lineHeight: 1.2,
|
|
},
|
|
children: line,
|
|
},
|
|
})),
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const [geistRegular, geistPixelSquare] = await Promise.all([
|
|
readFile(join(FONTS_DIR, "Geist-Regular.ttf")),
|
|
readFile(join(FONTS_DIR, "GeistPixel-Square.ttf")),
|
|
]);
|
|
|
|
const fonts = [
|
|
{ name: "Geist", data: geistRegular, weight: 400 as const, style: "normal" as const },
|
|
{ name: "Geist Pixel Square", data: geistPixelSquare, weight: 500 as const, style: "normal" as const },
|
|
];
|
|
|
|
for (const example of EXAMPLES) {
|
|
const svg = await satori(buildMarkup(example.title), {
|
|
width: WIDTH,
|
|
height: HEIGHT,
|
|
fonts,
|
|
});
|
|
|
|
const resvg = new Resvg(svg, {
|
|
fitTo: { mode: "width", value: WIDTH },
|
|
});
|
|
const png = resvg.render().asPng();
|
|
|
|
const outPath = getOutputPath(example);
|
|
await mkdir(dirname(outPath), { recursive: true });
|
|
await writeFile(outPath, png);
|
|
|
|
console.log(` ${example.dir} -> ${outPath.replace(ROOT + "/", "")}`);
|
|
}
|
|
|
|
console.log(`\nGenerated ${EXAMPLES.length} OG images.`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|