Sanity CMS Open Graph Tags: Dynamic OG Images with Next.js
Add og:title, og:image, og:description and Twitter card tags to a Next.js + Sanity site — including GROQ queries, portable text, and dynamic OG image generation.
Sanity + Next.js OG architecture
Sanity is a headless CMS that stores content in GROQ-queryable documents. Your Next.js frontend fetches content at build time (ISR/SSG) or runtime and sets OG tags via generateMetadata(). Sanity's image CDN (cdn.sanity.io) can also resize and transform images — perfect for OG image generation without a separate endpoint.
Step 1: Add an SEO object to your Sanity schema
// sanity/schemas/post.js
export default {
name: 'post',
type: 'document',
fields: [
{ name: 'title', type: 'string' },
{ name: 'slug', type: 'slug', options: { source: 'title' } },
{ name: 'excerpt', type: 'text' },
{ name: 'mainImage', type: 'image', options: { hotspot: true } },
{
name: 'seo',
type: 'object',
fields: [
{ name: 'metaTitle', type: 'string' },
{ name: 'metaDescription', type: 'text' },
{ name: 'ogImage', type: 'image', options: { hotspot: true } },
],
},
],
};Step 2: GROQ query + generateMetadata
// app/posts/[slug]/page.tsx
import { client } from '@/sanity/client';
import imageUrlBuilder from '@sanity/image-url';
import type { Metadata } from 'next';
const builder = imageUrlBuilder(client);
const urlFor = (source: any) => builder.image(source);
const POST_QUERY = `*[_type == "post" && slug.current == $slug][0]{
title, excerpt,
mainImage,
seo { metaTitle, metaDescription, ogImage }
}`;
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const post = await client.fetch(POST_QUERY, { slug: params.slug });
const ogTitle = post.seo?.metaTitle || post.title;
const ogDesc = post.seo?.metaDescription || post.excerpt;
const ogSrc = post.seo?.ogImage || post.mainImage;
// Use Sanity image CDN to resize to 1200×630
const ogImageUrl = ogSrc
? urlFor(ogSrc).width(1200).height(630).fit('crop').auto('format').url()
: undefined;
return {
title: ogTitle,
description: ogDesc,
openGraph: {
title: ogTitle,
description: ogDesc,
images: ogImageUrl ? [{ url: ogImageUrl, width: 1200, height: 630 }] : [],
type: 'article',
url: `https://yourdomain.com/posts/${params.slug}`,
},
twitter: {
card: 'summary_large_image',
title: ogTitle,
description: ogDesc,
images: ogImageUrl ? [ogImageUrl] : [],
},
};
}Using Sanity's image CDN as your OG image
Sanity's image pipeline (cdn.sanity.io/images/...) supports URL-based transforms. You can resize, crop, and format any uploaded image without a separate generation service:
// Direct Sanity image CDN URL for OG (no builder needed)
const ogImageUrl =
`https://cdn.sanity.io/images/${projectId}/${dataset}/${assetId}-${dimensions}.jpg`
+ `?w=1200&h=630&fit=crop&auto=format`;Common Sanity OG mistakes
- Image reference not resolved: GROQ returns image references by default — you need
urlFor()to get the actual URL. - CDN image is too small: Always pass
.width(1200).height(630)to the builder — og:image must be ≥600×315px. - Stale ISR cache: After updating OG fields in Sanity Studio, trigger an ISR revalidation or redeploy to regenerate the page.
Verify your Sanity OG tags
After deploying, paste your URL into OGFixer to preview exactly how Twitter, Slack, Discord, and LinkedIn will render your links.