December 28, 2025

Dynamic OpenGraph image generation

Dynamic OpenGraph image generation is achieved using the @cloudflare/pages-plugin-vercel-og.

Dynamic OpenGraph image generation

I'm searching for a better, customizable Open Graph image generator that integrates with Cloudflare.

I found the official Vercel/OG Cloudflare version!

install

pnpm add @cloudflare/pages-plugin-vercel-og

Use

Since it's deployed on Cloudflare, I'm unsure about compatibility with other platforms.

Og canvas

src\components\OGdefaultTemplate.tsx
import { ImageResponse } from "@cloudflare/pages-plugin-vercel-og/api";
import type { JSXElementConstructor, ReactElement } from "react";
export async function DefaultTemplate({
title,
description,
fontData,
}: {
title: string;
description: string;
fontData?: ArrayBuffer | null;
}): Promise<Response> {
const element: ReactElement<any, JSXElementConstructor<any>> = (
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "space-between",
backgroundColor: "#111", // Slightly lighter black for depth
padding: "80px",
color: "white",
fontFamily: fontData ? "Noto Sans SC" : "sans-serif",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "24px",
marginTop: "40px",
}}
>
<div
style={{
fontSize: "72px",
fontWeight: 800,
lineHeight: 1.1,
letterSpacing: "-0.02em",
maxWidth: "900px",
}}
>
{title}
</div>
<div
style={{
fontSize: "32px",
color: "#888",
lineHeight: 1.4,
maxWidth: "800px",
// Satori doesn't support -webkit-box for line clamping
// heavily relying on JS truncation or simple overflow hidden
overflow: "hidden",
whiteSpace: "pre-wrap", // Allow wrapping
display: "flex",
}}
>
{description.length > 100
? description.slice(0, 100) + "..."
: description}
</div>
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: "16px",
}}
>
<div
style={{
width: "48px",
height: "48px",
backgroundColor: "white",
borderRadius: "12px",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "28px",
color: "black",
fontWeight: 900,
}}
>
M
</div>
<div
style={{
fontSize: "28px",
fontWeight: 600,
letterSpacing: "-0.01em",
}}
>
MemoMemo
</div>
</div>
<div
style={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: "12px",
background: "linear-gradient(to bottom, #7928CA, #FF0080)",
}}
/>
</div>
);
return new ImageResponse(element, {
width: 1200,
height: 630,
fonts: fontData
? [
{
name: "Noto Sans SC",
data: fontData,
weight: 500,
style: "normal",
},
]
: [],
});
}

Head insert og image

src\components\BaseHead.astro
...
const OgImageUrl = new URL(
`/og/default.png?title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}`,
Astro.url
).href;
...
<meta property="og:image" content={OgImageUrl} />
...
<meta property="twitter:image" content={OgImageUrl} />

create API route

src\pages\og\[...template].ts
import type { APIRoute } from "astro";
import { DefaultTemplate } from "@/components/OGdefaultTemplate.tsx";
import { SITE } from "@/config";
export const prerender = false;
export const GET: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url);
const title = url.searchParams.get("title") || SITE.title;
const description = url.searchParams.get("description") || SITE.description;
// Fetch font data with error handling
let fontData: ArrayBuffer | null = null;
try {
const fontUrl = new URL(
"/fonts/noto-sans-sc-chinese-simplified-500-normal.woff",
request.url
);
const fontResponse = await fetch(fontUrl);
if (fontResponse.ok) {
fontData = await fontResponse.arrayBuffer();
} else {
console.warn(
`Failed to load font: ${fontResponse.status} ${fontResponse.statusText}`
);
}
} catch (fontError) {
console.warn("Font loading error:", fontError);
}
const imageResponse = await DefaultTemplate({
title,
description,
fontData,
});
// Workaround: Convert the stream to a buffer to ensure content differs from empty
const arrayBuffer = await imageResponse.arrayBuffer();
return new Response(arrayBuffer, {
status: 200,
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=31536000, immutable",
},
});
} catch (e: any) {
console.error("OG Generation Error:", e);
return new Response(`Failed to generate image: ${e.message}`, {
status: 500,
});
}
};

Check Og image

you can go to this url to check the og image