Implementing Dynamic OG Images with Cloudflare Pages: A Developer's Journey Through Trial and Error
Implementing dynamic Open Graph (OG) images can significantly improve your website’s social media presence. In this guide, I’ll share my journey of implementing dynamic OG images on an Astro website hosted on Cloudflare Pages.
I have always wanted to have dynamic og images on my previous websites, but I was way too lazy to implement it. Finally I decided to give it a try for my new website.
It turned out it was actually a lot of work if you don’t know what you are doing.
The Initial Optimism: Exploring Options
I started by considering the @vercel/og
package, which uses satori
and resvg
under the hood for image generation.
But then I thought let’s implement our own solution that uses the below packages
- satori for converting HTML/CSS to SVG
- @resvg/resvg-js for SVG to PNG conversion
“How hard could it be?” I thought.
After digging through some github repo, found a repo that is using satori and @resvg/resvg-js just the way I wanted.
After some tweaing and twisting, everything worked perfectly in my local environment, until I deployed it to cloudflare pages.
import { Resvg } from "@resvg/resvg-js";
import type { APIContext } from "astro";
import satori from "satori";
import fs from "fs/promises";
import path from "path";
import { templates } from "../../og-templates/templates";
export const prerender = false;
export async function GET(context: APIContext) {
const { template } = context.params;
if (!template) {
return new Response("Must provide a template", { status: 400 });
}
const templateFn = templates[template];
if (!templateFn) {
return new Response("Template not found", { status: 404 });
}
const { searchParams } = new URL(context.request.url);
const data = Object.fromEntries(searchParams.entries());
const svg = await satori(templateFn(data), {
width: 1200,
height: 630,
fonts: [
{
name: "Inter",
data: await fs.readFile(
path.join(process.cwd(), "src/og-templates/Inter-Regular.ttf"),
),
style: "normal",
},
],
});
const resvgInstance = new Resvg(svg, {
fitTo: {
mode: "width",
value: 1200,
},
});
const image = await resvgInstance.render();
return new Response(image.asPng());
}
Challenge #1: Node.js Compatibility in Cloudflare Runtime
The first major hurdle was Node.js compatibility. Despite Cloudflare’s Node.js compatibility mode, I encountered issues with basic Node.js features:
- Basic
fs
andreadFile
operations failed - Even after switching to
node:fs
imports and adding external dependencies in Vite - The
platform-proxy
flag didn’t resolve the runtime differences
Turns out even if platform-proxy
was enabled, vite still uses normal node js runtime to execute the code in local.
So even though it was working on local, it wasn’t going to work in cloudflare runtime.
I was about to let it go, but then I saw somewhere written that you have to use @resvg/resvg-wasm
package for cloudflare instead of @resvg/resvg-js
I was hopeful again.
Challenge #2: WebAssembly Integration
After discovering that Cloudflare required WebAssembly packages, I attempted to use resvg/resvg-wasm
. However, this led to a series of challenges.
I was getting errors like .initWasm() missing
, unkwnown file extension .wasm
etc. and many more errors, which I can’t even remeber anymore as it was very late at night.
I was again frustrated, decided it was not worth it to go through all this and will just use @vercel/og
package.
The Pivot: Leveraging AI for a Solution
I have been always skeptical about AI in development,since I could see myself getting addicted to it.
But this was not the time to overthink, I was just desperate to have a basic working implementation for now.
I decide to ask AI for help for a basic implementation of @vercel/og
package.
Initially I was going for a pretty sophisticated solutions with multiple templates, custom fonts etc. but made a complete hard reset and started from scratch again.
AI suggested a very basic implementation with a single template and a single font.
Everything was working fine in local, but when I deployed it to cloudflare pages, I was getting errors like Invalid urls in some chunk at line number something
.
I ignored it few times and decided to ask AI for help again.
It suggeested multiple things to do to my astro.conig.mjs
, Nothing worked.
So I decided to finally go throught the dist
folder and to find out about the exact line which producing the Invalid Url
error. I saw it where the package was trying to import the url for the font.
I was convinced we can’t use @vercel/og
package this way in cloudflare pages.
Before giving up, I decided to try one last time and read the cloudflare pages documentation for once more.
// Initial attempt that failed
import { ImageResponse } from "@vercel/og";
The Solution: The Documentation Enlightenment
Here’s where things got interesting.
After all the AI-suggested solutions, what actually solved my problem? Reading Cloudflare’s documentationCloudflare’s official documentation.
They provide a dedicated plugin @cloudflare/pages-plugin-vercel-og
that seamlessly integrates with Vercel’s OG image generation:
// The working solution
import { ImageResponse } from "@cloudflare/pages-plugin-vercel-og/api";
Implementation Details
Here’s the complete implementation using Cloudflare’s plugin:
- Template Component (
defaultTemplate.tsx
):
import { ImageResponse } from "@cloudflare/pages-plugin-vercel-og/api";
import type { JSXElementConstructor, ReactElement } from "react";
export async function DefaultTemplate({
title,
description,
}: {
title: string;
description: string;
}): Promise<Response> {
const element: ReactElement<any, JSXElementConstructor<any>> = (
<div>
<h1>{title}</h1>
<p>{description}</p>
</div>
);
return new ImageResponse(element, {
width: 1200,
height: 630,
});
}
- API Route (
og/[...template].ts
):
import type { APIRoute } from "astro";
import { DefaultTemplate } from "../components/og/templates/DefaultTemplate";
export const GET: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url);
const title = url.searchParams.get("title") || "Default Title";
const description = url.searchParams.get("description");
const response = await DefaultTemplate({ title, description });
response.headers.set("Content-Type", "image/png");
response.headers.set("Cache-Control", "public, max-age=31536000");
return response;
} catch (e) {
return new Response("Failed to generate image", { status: 500 });
}
};
Key Takeaways
- Start with Official Solutions: While custom implementations can be tempting, official solutions often provide the most reliable path forward.
- Understanding Runtime Environments: Cloudflare’s runtime differs significantly from local Node.js environments. Always check the platform documentation first.
- Documentation First: The solution often exists in the official documentation. Take time to thoroughly read platform-specific guides.
Additional Resources
- Cloudflare Pages OG Plugin Documentation
- Vercel OG Image Generation
- Astro Documentation
- Cloudflare Pages Platform Documentation
A Personal Note
Hey there! I’m Rakesh, an independent software contractor who loves exploring and sharing web development challenges. This journey with OG images is just one of the many technical adventures I embark on.
Feel free to comeback and check out my other posts or reach out to me on Twitter where I try to share development tips and insights. Until next time, happy coding! 👋