Created
July 9, 2026 06:10
-
-
Save igeligel/b6738f0c94fd32f95deef0aac51728fd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { ImageResponse } from "@vercel/og"; | |
| import type { NextApiRequest, NextApiResponse } from "next"; | |
| function dataUriToArrayBuffer(uri: string): ArrayBuffer { | |
| const match = uri.match( | |
| /^data:image\/(png|jpeg|webp|gif|svg\+xml);base64,(.+)$/, | |
| ); | |
| if (!match) | |
| throw new Error("Unsupported image format: expected data:image/*;base64,..."); | |
| const buf = Buffer.from(match[2], "base64"); | |
| return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); | |
| } | |
| let fontCache: Record<string, ArrayBuffer> | null = null; | |
| async function getInterFonts() { | |
| if (fontCache) { | |
| const c = fontCache; | |
| return [ | |
| { | |
| name: "Inter", | |
| data: c["400"], | |
| weight: 400 as const, | |
| style: "normal" as const, | |
| }, | |
| { | |
| name: "Inter", | |
| data: c["600"], | |
| weight: 600 as const, | |
| style: "normal" as const, | |
| }, | |
| { | |
| name: "Inter", | |
| data: c["700"], | |
| weight: 700 as const, | |
| style: "normal" as const, | |
| }, | |
| ]; | |
| } | |
| async function load(weight: number) { | |
| const cssUrl = `https://fonts.googleapis.com/css2?family=Inter:wght@${weight}&display=swap`; | |
| const css = await fetch(cssUrl).then((r) => r.text()); | |
| const match = css.match(/url\(([^)]+)\)/); | |
| if (!match) | |
| throw new Error(`Could not resolve font URL for Inter ${weight}`); | |
| const fontUrl = match[1].replace(/['"]/g, ""); | |
| return fetch(fontUrl).then((r) => r.arrayBuffer()); | |
| } | |
| const [w400, w600, w700] = await Promise.all([ | |
| load(400), | |
| load(600), | |
| load(700), | |
| ]); | |
| fontCache = { "400": w400, "600": w600, "700": w700 }; | |
| return [ | |
| { | |
| name: "Inter", | |
| data: w400, | |
| weight: 400 as const, | |
| style: "normal" as const, | |
| }, | |
| { | |
| name: "Inter", | |
| data: w600, | |
| weight: 600 as const, | |
| style: "normal" as const, | |
| }, | |
| { | |
| name: "Inter", | |
| data: w700, | |
| weight: 700 as const, | |
| style: "normal" as const, | |
| }, | |
| ]; | |
| } | |
| const TEAL = "#11998E"; | |
| const MINT = "#38EF7D"; | |
| const GREEN = "#0EB34D"; | |
| const INK = "#2C1702"; | |
| const SCALE = 2; | |
| const OG_WIDTH = 1200 * SCALE; | |
| const OG_HEIGHT = 630 * SCALE; | |
| const DEFAULT_POINTER_LEFT = 1002 * SCALE; | |
| const DEFAULT_POINTER_TOP = 408 * SCALE; | |
| const DEFAULT_POINTER_WIDTH = 52 * SCALE; | |
| const DEFAULT_POINTER_HEIGHT = 62 * SCALE; | |
| type Body = { | |
| cardImageSrc: string; | |
| companyName: string; | |
| logoSrc: string; | |
| heading: string; | |
| description: string; | |
| cardPointerLeftPx: number; | |
| cardPointerTopPx: number; | |
| }; | |
| function LogoMark() { | |
| const bars = [ | |
| { w: 9, h: 26, rotate: -10 }, | |
| { w: 11, h: 44, rotate: -4 }, | |
| { w: 10, h: 34, rotate: 6 }, | |
| { w: 9, h: 22, rotate: 14 }, | |
| { w: 8, h: 18, rotate: 22 }, | |
| ]; | |
| return ( | |
| <div | |
| style={{ | |
| display: "flex", | |
| alignItems: "flex-end", | |
| height: "72px", | |
| gap: "4px", | |
| }} | |
| > | |
| {bars.map((b, i) => ( | |
| <div | |
| key={i} | |
| style={{ | |
| width: `${b.w}px`, | |
| height: `${b.h}px`, | |
| borderRadius: "9999px", | |
| backgroundImage: `linear-gradient(to bottom, ${TEAL}, ${MINT})`, | |
| transform: `rotate(${b.rotate}deg)`, | |
| transformOrigin: "bottom center", | |
| }} | |
| /> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| function validateBody(raw: Record<string, unknown>): Body { | |
| const cardImageSrc = | |
| typeof raw.cardImageSrc === "string" && raw.cardImageSrc.trim() | |
| ? raw.cardImageSrc.trim() | |
| : ""; | |
| const heading = | |
| typeof raw.heading === "string" && raw.heading.trim() | |
| ? raw.heading.trim() | |
| : ""; | |
| const companyName = | |
| typeof raw.companyName === "string" ? raw.companyName : ""; | |
| const logoSrc = | |
| typeof raw.logoSrc === "string" | |
| ? raw.logoSrc | |
| : typeof raw.logoUrl === "string" | |
| ? raw.logoUrl | |
| : ""; | |
| const description = | |
| typeof raw.description === "string" ? raw.description : ""; | |
| const cardPointerLeftPx = | |
| typeof raw.cardPointerLeftPx === "number" && | |
| Number.isFinite(raw.cardPointerLeftPx) | |
| ? Math.max(0, Math.min(OG_WIDTH, raw.cardPointerLeftPx * SCALE)) | |
| : DEFAULT_POINTER_LEFT; | |
| const cardPointerTopPx = | |
| typeof raw.cardPointerTopPx === "number" && | |
| Number.isFinite(raw.cardPointerTopPx) | |
| ? Math.max(0, Math.min(OG_HEIGHT, raw.cardPointerTopPx * SCALE)) | |
| : DEFAULT_POINTER_TOP; | |
| return { | |
| cardImageSrc, | |
| heading, | |
| companyName, | |
| logoSrc, | |
| description, | |
| cardPointerLeftPx, | |
| cardPointerTopPx, | |
| }; | |
| } | |
| function truncated(s: string, max: number): string { | |
| return s.length <= max ? s : s.slice(0, max) + "…"; | |
| } | |
| export const maxDuration = 300; | |
| export const config = { | |
| api: { | |
| bodyParser: { | |
| sizeLimit: "4mb", | |
| }, | |
| }, | |
| }; | |
| /** | |
| * Renders a blogengine OG image server-side using @vercel/og (satori + resvg). | |
| * | |
| * POST /api/og-render-image | |
| * Body: { cardImageSrc, heading, companyName?, logoSrc?, description?, | |
| * cardPointerLeftPx?, cardPointerTopPx? } | |
| * Accepts logoSrc (or logoUrl for backwards compatibility). | |
| * | |
| * Returns: { image: "data:image/png;base64,...", width: 1200, height: 630 } | |
| */ | |
| export default async function handler( | |
| req: NextApiRequest, | |
| res: NextApiResponse, | |
| ) { | |
| if (req.method !== "POST") { | |
| res.setHeader("Allow", "POST"); | |
| return res.status(405).json({ error: "Method not allowed" }); | |
| } | |
| const body = validateBody(req.body); | |
| if (!body.heading) { | |
| return res.status(400).json({ error: "heading is required" }); | |
| } | |
| if (!body.cardImageSrc) { | |
| return res.status(400).json({ error: "cardImageSrc is required" }); | |
| } | |
| const tealPanelLeft = 433 * SCALE; | |
| const tealPanelLeftTop = -206.813 * SCALE; | |
| const tealPanelLeftWidth = 1113.87 * SCALE; | |
| const tealPanelLeftBottom = 1335.46 * SCALE; | |
| const brandLeft = 60 * SCALE; | |
| const brandTop = 160 * SCALE; | |
| const brandWidth = 540 * SCALE; | |
| const brandHeight = 72 * SCALE; | |
| const brandFontSize = 34 * SCALE; | |
| const brandGap = 14 * SCALE; | |
| const tealPanelRight = 570 * SCALE; | |
| const tealPanelRightTop = -200 * SCALE; | |
| const tealPanelRightWidth = 1000 * SCALE; | |
| const tealPanelRightBottom = 1500 * SCALE; | |
| const cardScreenshotLeft = 600 * SCALE; | |
| const cardScreenshotTop = 140 * SCALE; | |
| const cardScreenshotWidth = 750 * SCALE; | |
| const cardScreenshotHeight = 550 * SCALE; | |
| const cardScreenshotBorderRadius = 16 * SCALE; | |
| const headingWidth = 420 * SCALE; | |
| const headingFontSize = 52 * SCALE; | |
| const descriptionWidth = 450 * SCALE; | |
| const descriptionFontSize = 24 * SCALE; | |
| // | |
| const pointerFilterShadow = 4 * SCALE; | |
| const pointerFilter = `drop-shadow(0px ${pointerFilterShadow}px ${pointerFilterShadow}px rgba(0,0,0,0.22))`; | |
| try { | |
| const fonts = await getInterFonts(); | |
| const cardImageBuf = body.cardImageSrc.startsWith("data:") | |
| ? dataUriToArrayBuffer(body.cardImageSrc) | |
| : null; | |
| const logoBuf = body.logoSrc.startsWith("data:") | |
| ? dataUriToArrayBuffer(body.logoSrc) | |
| : null; | |
| const response = new ImageResponse( | |
| <div | |
| style={{ | |
| width: OG_WIDTH, | |
| height: OG_HEIGHT, | |
| display: "flex", | |
| position: "relative", | |
| backgroundColor: "white", | |
| overflow: "hidden", | |
| }} | |
| > | |
| {/* Rotated teal panel — left background */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| left: `${tealPanelLeft}px`, | |
| top: `${tealPanelLeftTop}px`, | |
| width: `${tealPanelLeftWidth}px`, | |
| height: `${tealPanelLeftBottom}px`, | |
| backgroundImage: `linear-gradient(135deg, ${TEAL}, ${MINT})`, | |
| transform: "rotate(-11.9264deg)", | |
| transformOrigin: "top left", | |
| zIndex: 0, | |
| }} | |
| /> | |
| <div | |
| style={{ | |
| position: "absolute", | |
| left: `${brandLeft}px`, | |
| top: `${brandTop}px`, | |
| display: "flex", | |
| flexDirection: "column", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| display: "flex", | |
| gap: `${brandGap}px`, | |
| alignItems: "center", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| maxWidth: `${brandWidth}px`, | |
| display: "flex", | |
| }} | |
| > | |
| {body.logoSrc.trim() ? ( | |
| <img | |
| src={(logoBuf ?? body.logoSrc) as unknown as string} | |
| style={{ | |
| maxWidth: `${brandWidth}px`, | |
| height: `${brandHeight}px`, | |
| objectFit: "contain", | |
| objectPosition: "left center", | |
| }} | |
| /> | |
| ) : ( | |
| <LogoMark /> | |
| )} | |
| </div> | |
| {body.companyName.trim() ? ( | |
| <div | |
| style={{ | |
| fontSize: `${brandFontSize}px`, | |
| fontWeight: 700, | |
| color: INK, | |
| fontFamily: "Inter", | |
| WebkitFontSmoothing: "antialiased", | |
| MozOsxFontSmoothing: "grayscale", | |
| textRendering: "optimizeLegibility", | |
| }} | |
| > | |
| {truncated(body.companyName, 40)} | |
| </div> | |
| ) : null} | |
| </div> | |
| <div | |
| style={{ | |
| maxWidth: `${headingWidth}px`, | |
| // color: TEAL, | |
| color: "transparent", | |
| backgroundImage: `linear-gradient(135deg, ${TEAL}, ${GREEN})`, | |
| backgroundClip: "text", | |
| WebkitBackgroundClip: "text", | |
| WebkitTextFillColor: "transparent", | |
| fontSize: `${headingFontSize}px`, | |
| fontWeight: 700, | |
| fontFamily: "Inter", | |
| WebkitFontSmoothing: "antialiased", | |
| MozOsxFontSmoothing: "grayscale", | |
| textRendering: "optimizeLegibility", | |
| lineHeight: 1.1, | |
| display: "flex", | |
| flexDirection: "column", | |
| zIndex: 2, | |
| marginTop: "1rem", | |
| paddingBottom: "0.2rem", | |
| }} | |
| > | |
| {truncated(body.heading, 120)} | |
| </div> | |
| {body.description.trim() ? ( | |
| <div | |
| style={{ | |
| marginTop: "1rem", | |
| maxWidth: `${descriptionWidth}px`, | |
| color: "transparent", | |
| backgroundImage: `linear-gradient(135deg, ${TEAL}, ${GREEN})`, | |
| backgroundClip: "text", | |
| WebkitBackgroundClip: "text", | |
| WebkitTextFillColor: "transparent", | |
| fontSize: `${descriptionFontSize}px`, | |
| fontWeight: 600, | |
| fontFamily: "Inter", | |
| WebkitFontSmoothing: "antialiased", | |
| MozOsxFontSmoothing: "grayscale", | |
| textRendering: "optimizeLegibility", | |
| lineHeight: 1.38, | |
| display: "flex", | |
| flexDirection: "column", | |
| zIndex: 2, | |
| }} | |
| > | |
| {truncated(body.description, 300)} | |
| </div> | |
| ) : null} | |
| </div> | |
| {/* Heading */} | |
| {/* Description */} | |
| {/* Rotated teal panel — right background */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| left: `${tealPanelRight}px`, | |
| top: `${tealPanelRightTop}px`, | |
| width: `${tealPanelRightWidth}px`, | |
| height: `${tealPanelRightBottom}px`, | |
| backgroundImage: `linear-gradient(180deg, ${TEAL}, ${MINT})`, | |
| transform: "rotate(-12deg)", | |
| zIndex: 3, | |
| }} | |
| /> | |
| {/* Card screenshot — right side, after teal panel */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| left: `${cardScreenshotLeft}px`, | |
| top: `${cardScreenshotTop}px`, | |
| width: `${cardScreenshotWidth}px`, | |
| height: `${cardScreenshotHeight}px`, | |
| borderRadius: "16px", | |
| overflow: "visible", | |
| transform: "rotate(-5deg)", | |
| transformOrigin: "top left", | |
| zIndex: 4, | |
| boxShadow: "0 4px 20px 0 rgba(0,0,0,0.25)", | |
| display: "flex", | |
| }} | |
| > | |
| <img | |
| src={(cardImageBuf ?? body.cardImageSrc) as unknown as string} | |
| style={{ | |
| overflow: "hidden", | |
| height: "100%", | |
| borderRadius: `${cardScreenshotBorderRadius}px`, | |
| objectFit: "cover", | |
| objectPosition: "left top", | |
| }} | |
| /> | |
| </div> | |
| {/* Pointer accent SVG */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| display: "flex", | |
| left: `${body.cardPointerLeftPx}px`, | |
| top: `${body.cardPointerTopPx}px`, | |
| zIndex: 5, | |
| filter: `${pointerFilter}`, | |
| }} | |
| > | |
| <svg | |
| width={`${DEFAULT_POINTER_WIDTH}px`} | |
| height={`${DEFAULT_POINTER_HEIGHT}px`} | |
| viewBox="1006 408 52 62" | |
| fill="none" | |
| xmlns="http://www.w3.org/2000/svg" | |
| style={{ display: "flex" }} | |
| > | |
| <path | |
| fillRule="evenodd" | |
| clipRule="evenodd" | |
| d="M1013.36 460.993L1006 413.515L1045.7 442.633L1025.6 445.75L1024.44 446.307L1013.36 460.993Z" | |
| fill="white" | |
| /> | |
| <path | |
| fillRule="evenodd" | |
| clipRule="evenodd" | |
| d="M1040.6 458.817L1030.62 465.024L1011.64 434.302L1021.86 428.004L1040.6 458.817Z" | |
| fill="white" | |
| /> | |
| <path | |
| fillRule="evenodd" | |
| clipRule="evenodd" | |
| d="M1036.34 457.412L1031.23 460.554L1018.65 440.118L1023.75 436.974L1036.34 457.412Z" | |
| fill="black" | |
| /> | |
| <path | |
| fillRule="evenodd" | |
| clipRule="evenodd" | |
| d="M1010.07 420.192L1015.21 453.359L1022.7 443.498L1023.9 442.89L1038.04 440.698L1010.07 420.192Z" | |
| fill="black" | |
| /> | |
| </svg> | |
| </div> | |
| </div>, | |
| { | |
| width: OG_WIDTH, | |
| height: OG_HEIGHT, | |
| fonts, | |
| }, | |
| ); | |
| const buffer = Buffer.from(await response.arrayBuffer()); | |
| const base64 = buffer.toString("base64"); | |
| return res.status(200).json({ | |
| image: `data:image/png;base64,${base64}`, | |
| width: OG_WIDTH, | |
| height: OG_HEIGHT, | |
| }); | |
| } catch (err) { | |
| const msg = err instanceof Error ? err.message : "Unknown error"; | |
| return res.status(500).json({ error: msg }); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment