|
| 1 | +import type { ReactNode } from "react"; |
| 2 | +import satori from "satori"; |
| 3 | +import { Resvg } from "@resvg/resvg-js"; |
| 4 | +import { readFile } from "node:fs/promises"; |
| 5 | + |
| 6 | +export const OG_WIDTH = 1200; |
| 7 | +export const OG_HEIGHT = 630; |
| 8 | + |
| 9 | +const HEADER_SVG = `<svg width="211" height="39" viewBox="0 0 211 39" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 10 | +<path d="M24.2644 38.64V9.44H29.1444V13.64H30.2644L29.1444 14.8C29.1444 13.0133 29.6777 11.6133 30.7444 10.6C31.811 9.56001 33.251 9.04 35.0644 9.04C37.2777 9.04 39.051 9.80001 40.3844 11.32C41.7177 12.84 42.3844 14.88 42.3844 17.44V23.4C42.3844 25.1067 42.0777 26.6 41.4644 27.88C40.8777 29.1334 40.0377 30.1067 38.9444 30.8C37.851 31.4934 36.5577 31.84 35.0644 31.84C33.251 31.84 31.811 31.3333 30.7444 30.32C29.6777 29.28 29.1444 27.8667 29.1444 26.08L30.2644 27.24H29.1044L29.2644 32.48V38.64H24.2644ZM33.3044 27.52C34.5844 27.52 35.5844 27.1467 36.3044 26.4C37.0244 25.6533 37.3844 24.5867 37.3844 23.2V17.68C37.3844 16.2933 37.0244 15.2267 36.3044 14.48C35.5844 13.7333 34.5844 13.36 33.3044 13.36C32.051 13.36 31.0644 13.7467 30.3444 14.52C29.6244 15.2667 29.2644 16.32 29.2644 17.68V23.2C29.2644 24.56 29.6244 25.6267 30.3444 26.4C31.0644 27.1467 32.051 27.52 33.3044 27.52Z" fill="white"/> |
| 11 | +<path d="M57.2088 31.84C55.3421 31.84 53.7154 31.48 52.3288 30.76C50.9421 30.04 49.8621 29.04 49.0887 27.76C48.3421 26.4534 47.9688 24.9334 47.9688 23.2V17.68C47.9688 15.9467 48.3421 14.44 49.0887 13.16C49.8621 11.8533 50.9421 10.84 52.3288 10.12C53.7154 9.4 55.3421 9.04 57.2088 9.04C59.0488 9.04 60.6488 9.4 62.0088 10.12C63.3954 10.84 64.4621 11.8533 65.2088 13.16C65.9821 14.44 66.3687 15.9467 66.3687 17.68V21.64H52.8088V23.2C52.8088 24.7467 53.1821 25.92 53.9287 26.72C54.6754 27.4934 55.7821 27.88 57.2487 27.88C58.3687 27.88 59.2754 27.6933 59.9688 27.32C60.6621 26.92 61.1021 26.3467 61.2887 25.6H66.2087C65.8354 27.4933 64.8221 29.0133 63.1688 30.16C61.5421 31.28 59.5554 31.84 57.2088 31.84ZM61.5288 18.84V17.64C61.5288 16.12 61.1687 14.96 60.4487 14.16C59.7287 13.3333 58.6488 12.92 57.2088 12.92C55.7688 12.92 54.6754 13.3333 53.9287 14.16C53.1821 14.9867 52.8088 16.16 52.8088 17.68V18.52L61.8888 18.44L61.5288 18.84Z" fill="white"/> |
| 12 | +<path d="M72.2331 31.44V9.44H77.1131V13.64H78.4731L77.1131 14.8C77.1131 12.9867 77.6331 11.5733 78.6731 10.56C79.7398 9.54667 81.1931 9.04 83.0331 9.04C85.1931 9.04 86.9131 9.76001 88.1931 11.2C89.4998 12.64 90.1531 14.5733 90.1531 17V31.44H85.1531V17.52C85.1531 16.1867 84.8065 15.16 84.1131 14.44C83.4198 13.72 82.4465 13.36 81.1931 13.36C79.9665 13.36 78.9931 13.7333 78.2731 14.48C77.5798 15.2267 77.2331 16.2933 77.2331 17.68V31.44H72.2331Z" fill="white"/> |
| 13 | +<path d="M107.537 31.44C105.431 31.44 103.777 30.8533 102.577 29.68C101.404 28.5067 100.817 26.8934 100.817 24.84V13.96H94.8575V9.44H100.817V3.24001H105.857V9.44H114.298V13.96H105.857V24.84C105.857 26.2267 106.537 26.92 107.897 26.92H113.897V31.44H107.537Z" fill="#CA2121"/> |
| 14 | +<path d="M126.682 31.84C124.415 31.84 122.629 31.24 121.322 30.04C120.015 28.84 119.362 27.2267 119.362 25.2C119.362 23.04 120.082 21.3733 121.522 20.2C122.962 19.0267 125.002 18.44 127.642 18.44H133.122V16.56C133.122 15.4933 132.775 14.6667 132.082 14.08C131.389 13.4667 130.442 13.16 129.242 13.16C128.149 13.16 127.242 13.4 126.522 13.88C125.802 14.36 125.375 15.0133 125.242 15.84H120.362C120.602 13.76 121.535 12.1067 123.162 10.88C124.789 9.65334 126.869 9.04 129.402 9.04C132.095 9.04 134.215 9.72001 135.762 11.08C137.335 12.4133 138.122 14.2267 138.122 16.52V31.44H133.282V27.6H132.482L133.282 26.52C133.282 28.1467 132.682 29.44 131.482 30.4C130.282 31.36 128.682 31.84 126.682 31.84ZM128.322 28.08C129.735 28.08 130.882 27.72 131.762 27C132.669 26.28 133.122 25.3467 133.122 24.2V21.52H127.722C126.709 21.52 125.895 21.8133 125.282 22.4C124.669 22.9867 124.362 23.76 124.362 24.72C124.362 25.76 124.709 26.5867 125.402 27.2C126.122 27.7867 127.095 28.08 128.322 28.08Z" fill="#CA2121"/> |
| 15 | +<path d="M144.146 31.44V26.88H151.826V13.96H145.146V9.44H156.626V26.88H163.426V31.44H144.146ZM153.826 5.76C152.813 5.76 152.013 5.50667 151.426 5C150.84 4.46667 150.546 3.76 150.546 2.88C150.546 2.00001 150.84 1.30666 151.426 0.799995C152.013 0.266663 152.813 0 153.826 0C154.84 0 155.64 0.266663 156.226 0.799995C156.813 1.30666 157.106 2.00001 157.106 2.88C157.106 3.76 156.813 4.46667 156.226 5C155.64 5.50667 154.84 5.76 153.826 5.76Z" fill="#CA2121"/> |
| 16 | +<path d="M168.171 31.44V9.44H173.051V13.64H174.411L173.051 14.8C173.051 12.9867 173.571 11.5733 174.611 10.56C175.677 9.54667 177.131 9.04 178.971 9.04C181.131 9.04 182.851 9.76001 184.131 11.2C185.437 12.64 186.091 14.5733 186.091 17V31.44H181.091V17.52C181.091 16.1867 180.744 15.16 180.051 14.44C179.357 13.72 178.384 13.36 177.131 13.36C175.904 13.36 174.931 13.7333 174.211 14.48C173.517 15.2267 173.171 16.2933 173.171 17.68V31.44H168.171Z" fill="#CA2121"/> |
| 17 | +<path d="M9.2002 9.08008C10.6403 9.0801 11.9261 9.2885 13.0576 9.70508C12.6861 10.3489 12.5 11.1138 12.5 12C12.5 13.375 12.9475 14.4792 13.8418 15.3125C14.7361 16.1041 15.9554 16.5 17.5 16.5C17.788 16.5 18.0647 16.4855 18.3301 16.458C18.3756 16.8519 18.4004 17.2591 18.4004 17.6797V23.1602C18.4004 24.92 18.0134 26.4532 17.2402 27.7598C16.4936 29.0397 15.44 30.0398 14.0801 30.7598C12.7201 31.4531 11.0934 31.7998 9.2002 31.7998C7.3336 31.7998 5.70695 31.453 4.32031 30.7598C2.96031 30.0398 1.89345 29.0398 1.12012 27.7598C0.373523 26.4532 1.94192e-05 24.92 0 23.1602V17.7197C3.9917e-05 15.9599 0.373515 14.4401 1.12012 13.1602C1.89345 11.8535 2.96031 10.8535 4.32031 10.1602C5.70694 9.44022 7.33361 9.08008 9.2002 9.08008ZM9.2002 13.4404C7.8937 13.4404 6.86676 13.7997 6.12012 14.5195C5.3735 15.2395 5.00005 16.3065 5 17.7197V23.1602C5.00002 24.5468 5.37348 25.6137 6.12012 26.3604C6.86677 27.0802 7.89366 27.4404 9.2002 27.4404C10.5334 27.4404 11.5603 27.0803 12.2803 26.3604C13.0269 25.6137 13.4004 24.5468 13.4004 23.1602V17.7197C13.4003 16.3065 13.0269 15.2395 12.2803 14.5195C11.5603 13.7996 10.5334 13.4405 9.2002 13.4404Z" fill="white"/> |
| 18 | +<path d="M17.4201 14.88C16.4067 14.88 15.6067 14.6267 15.0201 14.12C14.4334 13.5867 14.1401 12.88 14.1401 12C14.1401 11.12 14.4334 10.4267 15.0201 9.91999C15.6067 9.38666 16.4067 9.12 17.4201 9.12C18.4334 9.12 19.2334 9.38666 19.8201 9.91999C20.4067 10.4267 20.7001 11.12 20.7001 12C20.7001 12.88 20.4067 13.5867 19.8201 14.12C19.2334 14.6267 18.4334 14.88 17.4201 14.88Z" fill="#CA2121"/> |
| 19 | +<path d="M203.475 31.44C201.368 31.44 199.715 30.8533 198.515 29.68C197.342 28.5067 196.755 26.8934 196.755 24.84V13.96H190.795V9.44H196.755V3.24001H201.795V9.44H210.235V13.96H201.795V24.84C201.795 26.2267 202.475 26.92 203.835 26.92H209.835V31.44H203.475Z" fill="#CA2121"/> |
| 20 | +<path d="M153.826 5.76C152.813 5.76 152.013 5.50667 151.426 5C150.84 4.46667 150.546 3.76 150.546 2.88C150.546 2.00001 150.84 1.30666 151.426 0.799995C152.013 0.266663 152.813 0 153.826 0C154.84 0 155.64 0.266663 156.226 0.799995C156.813 1.30666 157.106 2.00001 157.106 2.88C157.106 3.76 156.813 4.46667 156.226 5C155.64 5.50667 154.84 5.76 153.826 5.76Z" fill="white"/> |
| 21 | +</svg>`; |
| 22 | + |
| 23 | +const HEADER_WIDTH = 422; |
| 24 | +const HEADER_HEIGHT = 78; |
| 25 | +const headerDataUri = `data:image/svg+xml;base64,${Buffer.from(HEADER_SVG).toString("base64")}`; |
| 26 | + |
| 27 | +let fontBold: ArrayBuffer | null = null; |
| 28 | +let fontRegular: ArrayBuffer | null = null; |
| 29 | + |
| 30 | +async function loadFonts() { |
| 31 | + if (!fontBold || !fontRegular) { |
| 32 | + const [bold, regular] = await Promise.all([ |
| 33 | + readFile("src/assets/fonts/JetBrainsMono-Bold.ttf"), |
| 34 | + readFile("src/assets/fonts/JetBrainsMono-Regular.ttf"), |
| 35 | + ]); |
| 36 | + fontBold = bold.buffer as ArrayBuffer; |
| 37 | + fontRegular = regular.buffer as ArrayBuffer; |
| 38 | + } |
| 39 | + return { bold: fontBold, regular: fontRegular }; |
| 40 | +} |
| 41 | + |
| 42 | +function h( |
| 43 | + type: string, |
| 44 | + style: Record<string, unknown>, |
| 45 | + ...children: (ReactNode | string)[] |
| 46 | +): ReactNode { |
| 47 | + return { |
| 48 | + type, |
| 49 | + props: { |
| 50 | + style, |
| 51 | + children: children.length === 1 ? children[0] : children, |
| 52 | + }, |
| 53 | + } as unknown as ReactNode; |
| 54 | +} |
| 55 | + |
| 56 | +function img(src: string, width: number, height: number): ReactNode { |
| 57 | + return { |
| 58 | + type: "img", |
| 59 | + props: { src, width, height }, |
| 60 | + } as unknown as ReactNode; |
| 61 | +} |
| 62 | + |
| 63 | +function shell( |
| 64 | + ...contentChildren: (ReactNode | string)[] |
| 65 | +): ReactNode { |
| 66 | + return h("div", { |
| 67 | + display: "flex", |
| 68 | + flexDirection: "column", |
| 69 | + width: "100%", |
| 70 | + height: "100%", |
| 71 | + backgroundColor: "#0a0c10", |
| 72 | + padding: "60px", |
| 73 | + fontFamily: "JetBrains Mono", |
| 74 | + position: "relative", |
| 75 | + overflow: "hidden", |
| 76 | + }, |
| 77 | + h("div", { |
| 78 | + display: "flex", |
| 79 | + position: "absolute", |
| 80 | + top: "0", |
| 81 | + left: "0", |
| 82 | + right: "0", |
| 83 | + height: "4px", |
| 84 | + background: "linear-gradient(90deg, #CA2121 0%, #CA2121 40%, transparent 100%)", |
| 85 | + }), |
| 86 | + h("div", { |
| 87 | + display: "flex", |
| 88 | + position: "absolute", |
| 89 | + top: "0", |
| 90 | + left: "0", |
| 91 | + width: "4px", |
| 92 | + height: "80px", |
| 93 | + backgroundColor: "#CA2121", |
| 94 | + }), |
| 95 | + h("div", { |
| 96 | + display: "flex", |
| 97 | + position: "absolute", |
| 98 | + top: "0", |
| 99 | + left: "0", |
| 100 | + right: "0", |
| 101 | + bottom: "0", |
| 102 | + backgroundImage: |
| 103 | + "radial-gradient(circle at 1px 1px, rgba(255,255,255,0.03) 1px, transparent 0)", |
| 104 | + backgroundSize: "32px 32px", |
| 105 | + }), |
| 106 | + h("div", { display: "flex", alignItems: "center" }, |
| 107 | + img(headerDataUri, HEADER_WIDTH, HEADER_HEIGHT), |
| 108 | + ), |
| 109 | + ...contentChildren, |
| 110 | + ); |
| 111 | +} |
| 112 | + |
| 113 | +export function buildPostImage(title: string, date: string): ReactNode { |
| 114 | + const formattedDate = new Date(date).toLocaleDateString("en-US", { |
| 115 | + year: "numeric", |
| 116 | + month: "short", |
| 117 | + day: "numeric", |
| 118 | + }); |
| 119 | + |
| 120 | + return shell( |
| 121 | + h("div", { |
| 122 | + display: "flex", |
| 123 | + fontSize: "46px", |
| 124 | + fontWeight: 700, |
| 125 | + color: "#ffffff", |
| 126 | + lineHeight: 1.15, |
| 127 | + letterSpacing: "-0.03em", |
| 128 | + marginTop: "auto", |
| 129 | + maxWidth: "1000px", |
| 130 | + }, title), |
| 131 | + |
| 132 | + h("div", { |
| 133 | + display: "flex", |
| 134 | + justifyContent: "space-between", |
| 135 | + alignItems: "center", |
| 136 | + marginTop: "28px", |
| 137 | + paddingTop: "20px", |
| 138 | + borderTop: "1px solid rgba(255,255,255,0.08)", |
| 139 | + }, |
| 140 | + h("div", { |
| 141 | + display: "flex", |
| 142 | + fontSize: "14px", |
| 143 | + fontWeight: 400, |
| 144 | + color: "#52525b", |
| 145 | + letterSpacing: "0.05em", |
| 146 | + textTransform: "uppercase", |
| 147 | + }, formattedDate), |
| 148 | + h("div", { |
| 149 | + display: "flex", |
| 150 | + fontSize: "14px", |
| 151 | + fontWeight: 400, |
| 152 | + color: "#52525b", |
| 153 | + letterSpacing: "0.05em", |
| 154 | + }, "opentaint.org"), |
| 155 | + ), |
| 156 | + ); |
| 157 | +} |
| 158 | + |
| 159 | +export function buildSiteImage(tagline: string): ReactNode { |
| 160 | + return shell( |
| 161 | + h("div", { |
| 162 | + display: "flex", |
| 163 | + fontSize: "46px", |
| 164 | + fontWeight: 700, |
| 165 | + color: "#ffffff", |
| 166 | + lineHeight: 1.15, |
| 167 | + letterSpacing: "-0.03em", |
| 168 | + marginTop: "auto", |
| 169 | + maxWidth: "1000px", |
| 170 | + }, tagline), |
| 171 | + |
| 172 | + h("div", { |
| 173 | + display: "flex", |
| 174 | + justifyContent: "flex-end", |
| 175 | + alignItems: "center", |
| 176 | + marginTop: "28px", |
| 177 | + paddingTop: "20px", |
| 178 | + borderTop: "1px solid rgba(255,255,255,0.08)", |
| 179 | + }, |
| 180 | + h("div", { |
| 181 | + display: "flex", |
| 182 | + fontSize: "14px", |
| 183 | + fontWeight: 400, |
| 184 | + color: "#52525b", |
| 185 | + letterSpacing: "0.05em", |
| 186 | + }, "opentaint.org"), |
| 187 | + ), |
| 188 | + ); |
| 189 | +} |
| 190 | + |
| 191 | +export async function renderOgImage(element: ReactNode): Promise<Uint8Array> { |
| 192 | + const fonts = await loadFonts(); |
| 193 | + |
| 194 | + const svg = await satori(element, { |
| 195 | + width: OG_WIDTH, |
| 196 | + height: OG_HEIGHT, |
| 197 | + fonts: [ |
| 198 | + { name: "JetBrains Mono", data: fonts.bold, weight: 700, style: "normal" }, |
| 199 | + { name: "JetBrains Mono", data: fonts.regular, weight: 400, style: "normal" }, |
| 200 | + ], |
| 201 | + }); |
| 202 | + |
| 203 | + const resvg = new Resvg(svg, { |
| 204 | + fitTo: { mode: "width", value: OG_WIDTH }, |
| 205 | + }); |
| 206 | + |
| 207 | + return resvg.render().asPng(); |
| 208 | +} |
| 209 | + |
| 210 | +export function ogResponse(png: Uint8Array): Response { |
| 211 | + return new Response(png, { |
| 212 | + headers: { |
| 213 | + "Content-Type": "image/png", |
| 214 | + "Cache-Control": "public, max-age=31536000, immutable", |
| 215 | + }, |
| 216 | + }); |
| 217 | +} |
0 commit comments