March 28, 2026 · 8 min read

Dynamic OG Images Without Next.js or Vercel

Most guides assume you're using Next.js and deploying to Vercel. But what if you're running Rails, Django, Laravel, Hugo, Astro, or a plain static site? Here's how to generate dynamic social preview images with any stack.

The problem with framework-specific solutions

Vercel's @vercel/og package is excellent — if you're already in the Vercel ecosystem. It uses Satori under the hood to render React components to images at the edge.

But it comes with real constraints:

There's a simpler approach: treat OG image generation as an external API call, just like you'd use Stripe for payments or Twilio for SMS.

The API approach: one URL, any stack

Instead of running image generation infrastructure, you can use a dedicated API. The idea is simple: construct a URL with your parameters, and the API returns a PNG.

Here's what that looks like with OGPeek:

The URL
https://ogpeek.com/api/v1/og?title=My+Blog+Post&subtitle=Published+March+2026&template=gradient&theme=midnight&brandColor=%23FF7A00

That URL is the image. Drop it into any <meta> tag:

HTML
<meta property="og:image"
  content="https://ogpeek.com/api/v1/og
    ?title=My+Blog+Post
    &template=gradient
    &theme=dark" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

That's it. No build step. No dependencies. No deployment pipeline. Works the same whether your site is built with Hugo, Jekyll, WordPress, Django, Rails, or hand-written HTML.

Example OG image generated by the API

Framework-by-framework examples

Ruby on Rails

In your layout or view, construct the URL dynamically from your model data:

<%# app/views/layouts/application.html.erb %>
<meta property="og:image"
  content="https://ogpeek.com/api/v1/og?title=<%= ERB::Util.url_encode(@post.title) %>&subtitle=<%= ERB::Util.url_encode(@post.published_at.strftime('%B %Y')) %>&template=gradient&theme=dark" />

Django / Python

Use Django's template engine with urlencode:

{# templates/blog/post.html #}
<meta property="og:image"
  content="https://ogpeek.com/api/v1/og?title={{ post.title|urlencode }}&subtitle={{ post.date|date:'F Y'|urlencode }}&template=gradient&theme=midnight" />

Laravel / PHP

In a Blade template:

{{-- resources/views/post.blade.php --}}
<meta property="og:image"
  content="https://ogpeek.com/api/v1/og?title={{ urlencode($post->title) }}&subtitle={{ urlencode($post->published_at->format('F Y')) }}&template=gradient&theme=dark" />

Hugo (Go templates)

Hugo's templating makes this straightforward:

{{/* layouts/partials/og.html */}}
<meta property="og:image"
  content="https://ogpeek.com/api/v1/og?title={{ .Title | urlquery }}&subtitle={{ .Date.Format "January 2006" | urlquery }}&template=gradient&theme=midnight" />

Astro

In your Astro layout's frontmatter, build the URL and pass it to the template:

---
// src/layouts/BlogPost.astro
const { title, date } = Astro.props;
const ogUrl = `https://ogpeek.com/api/v1/og?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(date)}&template=gradient&theme=dark`;
---
<meta property="og:image" content={ogUrl} />

Static sites / plain HTML

If you're generating pages with a script or a static site generator, just hardcode the URL with the right parameters per page. The API handles the rest.

Why this works so well

Social crawlers (Twitter, LinkedIn, Facebook, Slack) fetch the og:image URL when someone shares a link. They don't care how the image was generated. A URL that returns a PNG is a URL that returns a PNG, regardless of your backend technology.

Server-side generation with POST

For paid tiers (no watermark, higher limits), use a POST request with your API key. This is useful for pre-generating images in a build step or background job:

Node.js
const response = await fetch('https://ogpeek.com/api/v1/og', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.OGPEEK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    title: post.title,
    subtitle: post.date,
    template: 'gradient',
    theme: 'midnight',
    brandColor: '#FF7A00',
  }),
});

const png = Buffer.from(await response.arrayBuffer());
fs.writeFileSync(`public/og/${post.slug}.png`, png);
Python
import requests, os

response = requests.post(
    "https://ogpeek.com/api/v1/og",
    headers={"x-api-key": os.environ["OGPEEK_API_KEY"]},
    json={
        "title": post.title,
        "subtitle": post.published_at.strftime("%B %Y"),
        "template": "gradient",
        "theme": "midnight",
    }
)

with open(f"static/og/{post.slug}.png", "wb") as f:
    f.write(response.content)

When to use GET vs POST

Most teams start with GET in meta tags (zero infrastructure) and graduate to POST when they need watermark-free images or want to cache locally.

Performance comparison

Here's how the API approach compares to self-hosting:

Handling edge cases

Long titles

The API automatically truncates and adjusts font sizes for long titles. You don't need to handle this in your template.

Special characters

URL-encode your parameters. Most template engines have a built-in filter (urlencode, urlquery, encodeURIComponent). Ampersands, quotes, and Unicode all work correctly when encoded.

Caching

Social crawlers cache aggressively. If you change a title, the old image may persist for hours. Two strategies:

  1. Cache-busting parameter: append &v=2 to the URL when content changes.
  2. Pre-generate with POST: save the PNG to your own CDN and point the meta tag there.

Try it now

Generate your first OG image in under a minute. Free tier, no credit card.

Open the playground

Summary

You don't need Next.js, Vercel, or a headless browser to generate dynamic OG images. An API-based approach works with any tech stack, requires zero infrastructure, and takes five minutes to integrate.

The URL-as-API pattern is the same one that made services like Stripe and Twilio successful: do one thing well, expose it as a simple HTTP interface, and let developers integrate it however they want.

Learn more about OGPeek or read the full API docs.