How to Add Dynamic OG Images to Any Headless CMS
Headless CMS platforms like Contentful, Sanity, and Strapi give you structured content and API-first delivery. But they leave one problem unsolved: generating social preview images for every page. Manually creating OG images for hundreds of blog posts, product pages, and landing pages is not realistic. Here's how to automate it with a single API call.
The Headless CMS OG Image Problem
When you use a headless CMS, your content lives in one place and your frontend lives in another. The CMS stores your article titles, descriptions, categories, and author information. Your frontend—whether it is Next.js, Nuxt, Astro, SvelteKit, or a custom React app—fetches that content and renders pages.
Social preview images (OG images) sit awkwardly between these two layers. The CMS knows the content but cannot generate images. The frontend can render HTML but running a headless browser at build time or request time to screenshot a page is slow, fragile, and expensive. Most teams end up in one of three situations:
- No OG images at all — social shares show a blank preview or a generic fallback, reducing click-through rates by 30-50%
- A single static OG image — every page shares the same preview, making social feeds look repetitive and unhelpful
- Manual creation in Figma or Canva — works for 10 pages, completely impractical for 500
The solution is an OG image API that generates unique images dynamically from your content metadata. You pass the title and subtitle as URL parameters, and the API returns a 1200×630 PNG. No design tools, no headless browser, no build step.
How OGPeek Works with Headless CMS
OGPeek is a REST API. You construct a URL with query parameters, and it returns an image. The integration pattern is the same regardless of which CMS or frontend framework you use:
- Fetch your content from the CMS (title, subtitle, category)
- URL-encode the dynamic values
- Construct the OGPeek URL with those values as parameters
- Set the URL as your
og:imagemeta tag
The image is generated on-demand and cached. The first request takes 100-200ms. Subsequent requests for the same parameters are served from cache in under 50ms. Social platform crawlers (Twitter, Facebook, LinkedIn, Slack) receive a fully rendered image every time.
No build step required: Because OGPeek generates images at request time, adding 1,000 new CMS entries does not add a single second to your frontend build. The images are created when social platforms crawl your pages.
Contentful Integration
Contentful is the most popular headless CMS in the enterprise space. Here is how to add dynamic OG images to a Contentful-powered site using Next.js (the most common pairing), though the same approach works with any frontend.
Next.js App Router + Contentful
// app/blog/[slug]/page.tsx
import { getEntry } from '@/lib/contentful';
export async function generateMetadata({ params }) {
const post = await getEntry('blogPost', params.slug);
const ogImage = new URL('https://todd-agent-prod.web.app/api/v1/og');
ogImage.searchParams.set('title', post.fields.title);
ogImage.searchParams.set('subtitle', post.fields.category || 'Blog');
ogImage.searchParams.set('template', 'gradient');
ogImage.searchParams.set('theme', 'midnight');
ogImage.searchParams.set('brandColor', '#FF7A00');
return {
title: post.fields.title,
description: post.fields.excerpt,
openGraph: {
title: post.fields.title,
description: post.fields.excerpt,
images: [{ url: ogImage.toString(), width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
images: [ogImage.toString()],
},
};
}
Every blog post in Contentful now gets a unique OG image automatically. When your content editors publish a new article, the OG image exists the moment the page goes live. No manual image creation, no additional workflow steps.
Gatsby + Contentful
// src/templates/blog-post.js
export const Head = ({ data }) => {
const { title, category } = data.contentfulBlogPost;
const params = new URLSearchParams({
title,
subtitle: category || 'Blog',
template: 'gradient',
theme: 'midnight',
brandColor: '#FF7A00'
});
const ogImage = `https://todd-agent-prod.web.app/api/v1/og?${params}`;
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} />
</>
);
};
Sanity Integration
Sanity's real-time content lake and GROQ query language make it popular with developer-heavy teams. OGPeek integrates cleanly because Sanity's data is already structured—you just need to pass the right fields to the API URL.
Next.js + Sanity
// app/blog/[slug]/page.tsx
import { client } from '@/lib/sanity';
async function getPost(slug: string) {
return client.fetch(
`*[_type == "post" && slug.current == $slug][0]{
title, excerpt, "category": categories[0]->title
}`,
{ slug }
);
}
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
const ogParams = new URLSearchParams({
title: post.title,
subtitle: post.category || 'Sanity Blog',
template: 'split',
theme: 'midnight',
brandColor: '#F03E2F' // Sanity's red, or use your brand color
});
return {
openGraph: {
images: [{
url: `https://todd-agent-prod.web.app/api/v1/og?${ogParams}`,
width: 1200,
height: 630,
}],
},
};
}
Nuxt + Sanity
// pages/blog/[slug].vue
<script setup>
const route = useRoute();
const { data: post } = await useSanityQuery(
`*[_type == "post" && slug.current == $slug][0]{ title, excerpt }`,
{ slug: route.params.slug }
);
const ogParams = new URLSearchParams({
title: post.value.title,
subtitle: 'Blog',
template: 'gradient',
theme: 'midnight',
brandColor: '#FF7A00'
});
useHead({
meta: [
{ property: 'og:image', content: `https://todd-agent-prod.web.app/api/v1/og?${ogParams}` },
{ property: 'og:image:width', content: '1200' },
{ property: 'og:image:height', content: '630' },
{ name: 'twitter:card', content: 'summary_large_image' },
],
});
</script>
Strapi Integration
Strapi is the leading open-source headless CMS, often self-hosted. Because your Strapi API returns JSON, constructing the OGPeek URL from response data follows the same pattern.
Next.js + Strapi
// app/blog/[slug]/page.tsx
async function getPost(slug: string) {
const res = await fetch(
`${process.env.STRAPI_URL}/api/articles?filters[slug][$eq]=${slug}&populate=category`
);
const { data } = await res.json();
return data[0];
}
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
const { title } = post.attributes;
const category = post.attributes.category?.data?.attributes?.name || 'Blog';
const ogParams = new URLSearchParams({
title,
subtitle: category,
template: 'gradient',
theme: 'dark',
brandColor: '#4945FF' // Strapi's indigo, or your brand color
});
return {
openGraph: {
images: [{
url: `https://todd-agent-prod.web.app/api/v1/og?${ogParams}`,
width: 1200,
height: 630,
}],
},
};
}
Astro + Strapi
---
// src/pages/blog/[slug].astro
const { slug } = Astro.params;
const res = await fetch(
`${import.meta.env.STRAPI_URL}/api/articles?filters[slug][$eq]=${slug}`
);
const { data } = await res.json();
const post = data[0].attributes;
const ogParams = new URLSearchParams({
title: post.title,
subtitle: 'Blog',
template: 'gradient',
theme: 'midnight',
brandColor: '#FF7A00'
});
const ogImage = `https://todd-agent-prod.web.app/api/v1/og?${ogParams}`;
---
<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" />
</head>
CMS Comparison: OG Image Approaches
Here is how the three major headless CMS platforms handle OG images natively versus with OGPeek:
| CMS | Native OG Image Support | With OGPeek |
|---|---|---|
| Contentful | Manual image upload per entry | Automatic from title field |
| Sanity | Manual image upload per document | Automatic from any GROQ field |
| Strapi | Manual media upload per content type | Automatic from API response |
| WordPress (headless) | Yoast SEO plugin (manual) | Automatic from post title |
| Prismic | Manual image field per document | Automatic from document title |
| DatoCMS | SEO field with manual image | Automatic from record title |
Every headless CMS supports adding an image field to your content model. But "supports" means your content editors must manually create and upload an OG image for every single entry. With OGPeek, the image is derived from existing fields. No extra work for anyone.
Add OG images to your headless CMS in 5 minutes
50 images per day on the free tier. No signup, no credit card. Works with Contentful, Sanity, Strapi, Prismic, and any CMS with an API.
Get started free →Advanced Patterns
Using CMS Categories as Template Themes
Map your CMS categories to different OGPeek templates or themes so that blog posts, case studies, and product updates each have a distinct visual style in social feeds:
// Map content types to OGPeek templates
const templateMap = {
'Engineering': { template: 'gradient', theme: 'midnight' },
'Product': { template: 'split', theme: 'dark' },
'Case Study': { template: 'minimal', theme: 'light' },
'Announcement': { template: 'bold', theme: 'midnight' },
};
function getOgImage(title, category) {
const config = templateMap[category] || templateMap['Engineering'];
const params = new URLSearchParams({
title,
subtitle: category,
...config,
brandColor: '#FF7A00'
});
return `https://todd-agent-prod.web.app/api/v1/og?${params}`;
}
Webhook-Based Cache Warming
For maximum performance, trigger a request to your OGPeek URL whenever content is published. This ensures the image is already cached before any social platform crawls the page:
// Contentful webhook handler (or Sanity/Strapi equivalent)
export async function POST(request) {
const payload = await request.json();
const title = payload.fields.title['en-US'];
const subtitle = payload.fields.category?.['en-US'] || 'Blog';
// Warm the OGPeek cache
const ogUrl = new URL('https://todd-agent-prod.web.app/api/v1/og');
ogUrl.searchParams.set('title', title);
ogUrl.searchParams.set('subtitle', subtitle);
ogUrl.searchParams.set('template', 'gradient');
ogUrl.searchParams.set('theme', 'midnight');
ogUrl.searchParams.set('brandColor', '#FF7A00');
await fetch(ogUrl.toString());
return new Response('OK', { status: 200 });
}
This is optional but recommended for high-traffic sites. The first social share of a new article will load the OG image from cache instead of generating it on the fly.
Why Not Use the CMS Image CDN?
Both Contentful and Sanity include image CDNs (Contentful Images API, Sanity Image Pipeline). These are excellent for photos, illustrations, and media assets. They are not designed for generating text-based social preview images.
You could theoretically create an OG image template in your CMS as a media asset, but you would need a separate image for every page. At 500 blog posts, that is 500 manually created images. OGPeek generates them automatically from your existing content fields.
Conclusion
Headless CMS platforms separate content from presentation. OG image generation should follow the same principle: your content metadata (title, category, author) drives the image, and the rendering is handled by a specialized API.
OGPeek fits naturally into headless CMS architectures because it is itself an API. Fetch your content from Contentful, Sanity, or Strapi. Pass the title to OGPeek. Set the meta tag. Every page gets a unique, professionally designed social preview image without any manual work from your content team.
The integration takes under 10 minutes for any CMS and any frontend framework. The free tier covers 50 images per day—enough to test, validate, and ship before you need a paid plan.
Read more: Dynamic OG images without Next.js, OGPeek integration for Next.js, Remix, and Astro, or see the full API documentation. For comparisons with other tools, see OGPeek vs Vercel OG and Cloudinary vs OGPeek.