ScreenshotNeo

BlogComparisons

Open Graph image generation with Cloudflare Workers vs Vercel

Compare Vercel’s documented ImageResponse workflow with the choices for Cloudflare Workers, then build, cache, and troubleshoot dynamic Open Graph images.

By the ScreenshotNeo team4 October 202610 min read

For an existing Next.js app on Vercel, start with Vercel’s documented ImageResponse workflow. It is the most direct, first-party route for generating Open Graph images. Cloudflare Workers can return image responses, but Cloudflare’s named @vercel/og integration is documented for Pages Functions, not Workers. For a Workers implementation, choose a renderer that works in the Workers runtime and validate its resource use, font support, and caching yourself. The available documentation does not establish a head-to-head performance or cost winner.

This guide compares the runtimes, walks through a runnable Next.js implementation, outlines the decisions needed for a Workers implementation, and covers crawler delivery, caching, limits, troubleshooting, and operational cost.

1. Choose by runtime and rendering needs

Question Vercel / Next.js Cloudflare Workers
What is the direct route? Use ImageResponse from next/og in a Next.js App Router route, or @vercel/og for other supported setups. Return an image Response from a Worker using a renderer compatible with that runtime. Cloudflare’s @cloudflare/pages-plugin-vercel-og is for Pages Functions.
What is it best suited to? Projects already using Next.js/Vercel that want the documented social-card workflow. Projects already deployed on Workers, where a compatible renderer and its resource use have been verified.
Layout and assets The documented renderer supports flexbox and a subset of CSS, TTF/OTF/WOFF fonts, and states a 500 KB maximum bundle including code and assets. Depends on the renderer selected. Do not assume Vercel’s documented CSS support or bundle constraints apply identically.
Caching The OG guide describes CDN caching; the API reference documents cache headers, with a long-lived immutable default. For Images binding transformations, responses are not automatically cached; Cloudflare recommends Workers Cache for repeat transformations.

Vercel’s guide recommends a 1200 × 630 image. It also says advanced CSS Grid layouts are unsupported and recommends allowing the image route in robots.txt so social crawlers can fetch it. Review the current documentation for runtime and version requirements before adopting its examples: the cited guide states Node.js 22 or newer for the described setup and Next.js 12.2.3 or newer for Next.js implementations, with caveats by router and runtime.

Cloudflare’s Pages plugin uses Vercel’s @vercel/og library and can extract webpage metadata, inject OG metadata, or render images. That is useful if the target is Pages Functions, but it is not a Workers-specific integration. Cloudflare Workers do implement the Fetch API Response interface and can return image bytes. Cloudflare’s Images binding handles image inputs, transformations, overlays, and output formats; it is an image processing tool rather than a drop-in dynamic HTML/CSS card renderer. Its documentation accepts image bytes up to 20 MB.

2. Generate an image in Next.js with Vercel ImageResponse

In an App Router project, create app/api/og/route.tsx. This example uses a query parameter for the title and supplies a fallback. Keep user-provided values bounded and escape or validate them as appropriate for the content you accept.

import { ImageResponse } from 'next/og';

export const runtime = 'edge';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const rawTitle = searchParams.get('title') ?? 'A useful page title';
  const title = rawTitle.slice(0, 120);

  return new ImageResponse(
    (
      <div
        style={{
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'space-between',
          padding: 64,
          background: '#101827',
          color: 'white',
          fontSize: 64,
          fontWeight: 700,
        }}
      >
        <div style={{ color: '#93c5fd', fontSize: 26 }}>Engineering notes</div>
        <div>{title}</div>
        <div style={{ color: '#cbd5e1', fontSize: 22 }}>example.com</div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

Visit /api/og?title=Rendering%20at%20the%20edge to see the generated PNG. The response is suitable as an image endpoint; a page still needs to publish its absolute URL in metadata. For example, in a route’s metadata:

export const metadata = {
  openGraph: {
    images: [
      {
        url: 'https://example.com/api/og?title=Rendering%20at%20the%20edge',
        width: 1200,
        height: 630,
        alt: 'Rendering at the edge',
      },
    ],
  },
};

The exact metadata arrangement depends on the Next.js version and whether metadata is static or generated per request. The key delivery requirements are a public absolute image URL, an image response with the correct content type, and crawler access. The Vercel guide describes ImageResponse options including dimensions, font data, status, and response headers; use the API reference for the current option shape and defaults.

Fonts and custom response behavior

For brand fonts, load font bytes and pass font configuration through the ImageResponse options. The documented supported formats are TTF, OTF, and WOFF. Include fonts and other assets in bundle-size accounting. Set response headers deliberately when you need a cache policy that differs from the library default, and ensure the content type remains an image type expected by social crawlers. Avoid fetching large or untrusted external assets during every render.

3. Plan a Cloudflare Workers implementation

There is no equivalent Workers-specific ImageResponse recipe established by the sources reviewed here. The implementation is therefore a renderer choice and compatibility exercise. A Worker can return a Response containing image bytes, but returning bytes is only the last step; the rendering package must function under the Workers runtime.

  1. Select a renderer for Workers. Confirm its runtime APIs, font loading approach, output format, and license from its official documentation. The Cloudflare Pages plugin documentation does not establish that the plugin itself runs in Workers.
  2. Build a minimal card first. Render fixed text and a background, then add fonts, remote data, and layout complexity incrementally.
  3. Return a correct response. Set an image content type, such as image/png, and an explicit cache policy appropriate to the URL’s inputs and update schedule.
  4. Measure resource use under the actual plan. Cloudflare currently lists a 10 ms CPU ceiling per request and 100,000 requests per day for Workers Free, a five-minute CPU ceiling for Paid, and 128 MB memory. These ceilings do not tell you how much CPU a chosen image renderer will consume.
  5. Make cache identity deterministic. Every input that changes pixels must affect the cache key, or stale/wrong cards can be served. Use a version or content revision in the URL when source content changes.

Cloudflare documents a Rust Worker tutorial that renders text to PNG with the Rust text-to-png package. It demonstrates image-byte generation in a Worker, not a ready-made HTML/CSS Open Graph service. Cloudflare’s Images binding and the cf.image fetch-subrequest path address image transformations and optimization; they are distinct approaches, and neither should be described as the Pages Vercel OG plugin.

4. Publish metadata and make the card crawler-accessible

  • Use a fully qualified public URL in og:image, including the scheme and host.
  • Serve the actual image at that URL, with a correct image content type and a successful response when requested without a browser session.
  • Keep the route accessible to social crawlers. Vercel’s guide recommends allowing the OG route in robots.txt.
  • Set width, height, and useful alt text in metadata where your framework supports them.
  • Check that authentication, geolocation restrictions, bot rules, or application middleware do not block crawler requests.
  • Make dynamic image URLs stable enough to cache and unique enough to represent their inputs.

Social platforms may cache fetched cards independently. Changing the generator’s response does not guarantee an already-cached preview will refresh immediately; use the platform’s supported re-fetch/debug mechanism when validating a changed card.

5. Cache deliberately

Generated cards are often requested repeatedly for the same content, so cache design affects latency, compute, and freshness. Vercel’s OG documentation describes generated-image CDN caching, and its API reference documents response headers with a long-lived immutable cache default. If a title or image can change while the URL remains constant, an immutable policy risks stale results. Prefer versioned URLs or a deliberate revalidation policy for mutable content.

For Cloudflare Images binding transformations, Cloudflare says responses are not automatically cached and recommends Workers Cache for repeated transformations. The same general rule applies to any dynamic Worker renderer: determine whether the response is cached, choose a key that includes every visual input, and decide how changes invalidate old output. Do not assume a transformation binding’s caching behavior applies to a separately implemented renderer.

6. Performance, reliability, and cost

Performance

Measure cold and warm requests for your own card template. Include font and asset loading, data lookup, render time, response size, and cache hit rate. External image or font fetches can add latency and failure points, so bundle stable assets when feasible and keep rendering inputs small. No reviewed source supplies a fair Cloudflare-versus-Vercel benchmark. Vercel’s 2022 launch announcement reports an improvement over its own prior generator using Vercel documentation traffic; that vendor-reported result is not an independent comparison and should not be used to claim one platform beats the other.

Reliability

  • Provide a fallback title and handle missing or malformed query parameters.
  • Bound user input length and avoid letting arbitrary user HTML or URLs control rendering.
  • Keep a simple fallback card or previously generated image available if dynamic data is temporarily unavailable.
  • Test actual crawler access from outside authenticated or preview environments.
  • Track render failures separately from cache hits and expected not-found responses.

Cost

The reviewed material does not support a current total-cost winner. Cost depends on the platform’s current pricing and quotas, request volume, cache hit rate, renderer resource consumption, and any external data or image services. Check the current platform pricing pages before committing. For a meaningful comparison, replay the same representative URL mix and cache conditions, then compare total billed usage and failure rate; do not infer cost from CPU ceilings alone.

7. Troubleshooting

Symptom Likely cause Fix
Social preview has no image The metadata URL is relative, private, blocked, or returns an error. Publish an absolute public URL, verify crawler access, allow the route in robots rules, and inspect the response status.
Image downloads as an error page Render threw an exception or middleware redirected the request. Check the route directly, inspect logs and status, add safe fallbacks for missing data, and exclude the image route from unsuitable auth redirects.
Layout differs from browser HTML The OG renderer supports a limited CSS subset. Use flexbox and supported styles, simplify the composition, and avoid advanced CSS Grid assumptions.
Custom font is missing or falls back Unsupported font format, incorrect bytes, failed fetch, or font omitted from configuration. Use TTF, OTF, or WOFF as documented, validate the loaded bytes, and pass the font data using the current API shape.
Vercel build rejects the route or dependency Runtime or framework version does not match the documented setup, or bundle exceeds the stated size limit. Check the current runtime caveats, use a supported configuration, and reduce bundled assets; the guide states a 500 KB maximum for code and assets.
Cloudflare Worker cannot import the renderer The package expects Node APIs or another runtime capability unavailable in Workers. Choose a Workers-compatible renderer and validate it in the actual Worker runtime; do not assume the Pages plugin is portable to Workers.
Worker exceeds CPU or memory limits Rendering, font parsing, image decoding, or external work exceeds plan limits. Profile a minimal template, reduce assets and work per request, cache generated results, and compare use with current Workers limits.
Updated card remains old Browser/CDN or social platform cached the prior URL. Version the URL when pixels change, use suitable cache headers, and request a refresh through the social platform’s supported tooling.
Cloudflare image transformation repeats on every request Images binding output is not automatically cached. Enable Workers Cache for repeat transformations and select a key that captures all transformation inputs.

8. Or skip the browser setup

If you need a screenshot of a live page for a report, preview, or agent workflow, ScreenshotNeo is a website screenshot API and MCP server. It is a screenshot alternative, rather than an OG image renderer: it captures a page URL as PNG, JPEG, WebP, or PDF. One GET request returns the capture. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Cookie banners are accepted and removed before capture, along with known newsletter popups and chat widgets. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for the free plan.

9. Frequently asked questions

Can I use Cloudflare’s Vercel OG plugin in a Worker?

The cited Cloudflare documentation describes @cloudflare/pages-plugin-vercel-og for Pages Functions. It does not establish it as a Workers plugin. Treat a Worker setup as a separate renderer integration.

Does a 1200 × 630 image have to be generated on every page request?

No. The URL can point to a generated endpoint with caching, or to a pre-generated static image. Choose based on how dynamic the content is and how quickly changes must appear.

Does the comparison identify a faster or cheaper platform?

No. The reviewed sources provide neither a fair head-to-head benchmark nor enough pricing information for a total-cost conclusion. Benchmark your renderer and verify current plan pricing and quotas.

Is ScreenshotNeo a replacement for an OG card renderer?

No. It captures rendered web pages as image or PDF outputs. Use it when the desired result is a screenshot of a live page; use an OG renderer to compose a purpose-built social card from content and design.