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-ogUse
Since it's deployed on Cloudflare, I'm unsure about compatibility with other platforms.
Og canvas
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
...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
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