March 28, 2026 · 7 min read

OG Images for Static Sites: Hugo, Gatsby, Jekyll, Astro & Eleventy

Static site generators produce fast, secure, deployable-anywhere websites. But they have one blind spot: there's no server to generate dynamic images at request time. Here's how to add unique Open Graph images to every page of your static site using a simple API—with integration code for the five most popular frameworks.

Why Static Sites Need an OG Image API

With a dynamic web application (Rails, Django, Express), you can generate OG images on the fly when a social platform's crawler requests them. Your server receives the request, renders the image, and returns it.

Static sites don't have that luxury. After the build step, you have a folder of HTML, CSS, and JS files sitting on a CDN. There is no server to intercept requests and render images dynamically.

This leaves you with three options:

  1. Create images manually in Figma for every page. Does not scale beyond a handful of pages.
  2. Generate images at build time using Puppeteer or Playwright. Adds minutes to your build, requires a headless browser in your CI pipeline, and breaks frequently.
  3. Point your meta tags at an API that generates images on demand. No build step changes, no server, works instantly.

Option 3 is what we'll focus on. You embed an API URL in your og:image meta tag during the build, and the API generates the image when a social platform (or anyone) requests it. Your static site stays static. The image generation happens externally.

Key insight: Social platforms don't care where your OG image is hosted. They follow the URL in your og:image tag and fetch whatever is there. That URL can be a static PNG on your CDN or an API endpoint that returns a dynamically generated PNG. The crawler can't tell the difference.

Hugo

Hugo uses Go templates. The integration goes in your base template or a dedicated partial. Create a file at layouts/partials/og-image.html:

{{ $title := .Title | urlquery }}
{{ $subtitle := .Site.Title | urlquery }}
{{ $ogURL := printf "https://ogpeek.com/api/v1/og?title=%s&subtitle=%s&template=gradient&theme=dark&brandColor=%%23FF7A00" $title $subtitle }}

<meta property="og:image" content="{{ $ogURL }}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="{{ $ogURL }}" />

Then include it in your layouts/_default/baseof.html inside the <head>:

{{ partial "og-image.html" . }}

Every page on your Hugo site now gets a unique OG image based on its title. Blog posts, documentation pages, landing pages—all covered automatically.

Gatsby

Gatsby uses React and provides a Head export for managing meta tags. In your page or template component:

// src/templates/blog-post.js
export const Head = ({ pageContext }) => {
  const ogImage = `https://ogpeek.com/api/v1/og?title=${
    encodeURIComponent(pageContext.title)
  }&subtitle=${
    encodeURIComponent('yourdomain.com')
  }&template=gradient&theme=midnight&brandColor=%23FF7A00`;

  return (
    <>
      <meta property="og:image" content={ogImage} />
      <meta property="og:image:width" content="1200" />
      <meta property="og:image:height" content="630" />
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:image" content={ogImage} />
    </>
  );
};

If you're using Gatsby's older react-helmet approach, the same meta tags go inside a <Helmet> component. The OGPeek URL works the same either way.

Jekyll

Jekyll uses Liquid templates. Add the following to your _includes/head.html (or wherever your <head> is defined):

{% assign og_title = page.title | default: site.title | url_encode %}
{% assign og_subtitle = site.title | url_encode %}
{% assign og_image = "https://ogpeek.com/api/v1/og?title=" | append: og_title | append: "&subtitle=" | append: og_subtitle | append: "&template=gradient&theme=dark&brandColor=%23FF7A00" %}

<meta property="og:image" content="{{ og_image }}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="{{ og_image }}" />

This works with both GitHub Pages and self-hosted Jekyll. Since the image is generated by OGPeek's API—not during your build—there are no additional gem dependencies or build plugins to configure.

Astro

Astro supports a <head> section directly in .astro layout files. Create or update your base layout:

---
// src/layouts/BaseLayout.astro
const { title, description } = Astro.props;
const ogImage = `https://ogpeek.com/api/v1/og?title=${
  encodeURIComponent(title)
}&subtitle=${
  encodeURIComponent('yourdomain.com')
}&template=gradient&theme=midnight&brandColor=%23FF7A00`;
---

<html lang="en">
<head>
  <meta property="og:image" content={ogImage} />
  <meta property="og:image:width" content="1200" />
  <meta property="og:image:height" content="630" />
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:image" content={ogImage} />
  <!-- rest of your head tags -->
</head>
<body>
  <slot />
</body>
</html>

Astro's component model makes this particularly clean. The title prop flows in from each page and gets encoded into the OGPeek URL automatically.

Eleventy (11ty)

Eleventy uses Nunjucks, Liquid, or JavaScript templates. Here's the Nunjucks approach. In your base layout (_includes/base.njk):

<meta property="og:image"
  content="https://ogpeek.com/api/v1/og?title={{ title | urlencode }}&subtitle={{ metadata.title | urlencode }}&template=gradient&theme=dark&brandColor=%23FF7A00" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image"
  content="https://ogpeek.com/api/v1/og?title={{ title | urlencode }}&subtitle={{ metadata.title | urlencode }}&template=gradient&theme=dark&brandColor=%23FF7A00" />

If you use Eleventy's JavaScript data cascade, you can also build the URL in a computed data file and pass it as a variable, which keeps your templates cleaner.

Customizing Your Brand

Across all five frameworks, the integration pattern is identical: construct an OGPeek API URL with your page title and brand parameters. Here are the parameters worth customizing:

Here's what a customized image looks like:

Example OG image for static site blog post

Pro tip: Use the subtitle parameter to include your domain name on every card. This builds brand recognition in social feeds even before someone clicks through to your site.

Try it with your static site

Open the playground, enter a blog post title, and see the generated image. Copy the URL and drop it into your template. No signup required for the free tier.

Open the playground →

Conclusion

Static site generators are the best way to build fast, secure websites. But the lack of a runtime server has historically made dynamic OG images a pain point—requiring build-time image generation with headless browsers, or falling back to a single generic image across the entire site.

An API-based approach solves this cleanly. You add a URL to your template, pass the page title as a parameter, and every page gets a unique, branded social preview image. The integration is a few lines of code regardless of whether you use Hugo, Gatsby, Jekyll, Astro, or Eleventy.

Your static site stays static. Your OG images stay dynamic.

More developer APIs from the Peek Suite