Contentful Open Graph Tags: Add OG to Your Headless CMS Site
How to add og:title, og:image, og:description and Twitter card meta tags when using Contentful as a headless CMS — with Next.js, Gatsby, or Nuxt examples.
Contentful content model for SEO
Since Contentful is a headless CMS, it doesn't render HTML itself — your frontend framework (Next.js, Gatsby, Nuxt) is responsible for outputting OG meta tags. The key is to create the right content model fields in Contentful, then map them to meta tags in your frontend.
# Recommended Contentful content model fields for SEO: # # Content Type: "Blog Post" # ───────────────────────────────── # Field Name │ Type │ Notes # ────────────────┼─────────────┼────────────────────── # title │ Short text │ Used for og:title # slug │ Short text │ Used to build og:url # excerpt │ Long text │ Used for og:description # ogImage │ Media │ Used for og:image (1200×630) # body │ Rich Text │ Main content # # You can also create a reusable "SEO" content type: # Content Type: "SEO Fields" # ───────────────────────────────── # ogTitle │ Short text │ Override title # ogDescription │ Long text │ Override description # ogImage │ Media │ Override image # noIndex │ Boolean │ Block indexing
Next.js + Contentful: generateMetadata
With Next.js App Router, use generateMetadata to fetch Contentful data and return OG tags:
// app/blog/[slug]/page.tsx
import { createClient } from "contentful";
const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
});
async function getPost(slug: string) {
const entries = await client.getEntries({
content_type: "blogPost",
"fields.slug": slug,
limit: 1,
});
return entries.items[0];
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
const { title, excerpt, ogImage } = post.fields;
const imageUrl = ogImage
? `https:${ogImage.fields.file.url}?w=1200&h=630&fit=fill`
: "https://yourdomain.com/og/default.png";
return {
title,
description: excerpt,
openGraph: {
title,
description: excerpt,
type: "article",
url: `https://yourdomain.com/blog/${params.slug}`,
images: [{ url: imageUrl, width: 1200, height: 630 }],
},
twitter: {
card: "summary_large_image",
title,
description: excerpt,
images: [imageUrl],
},
};
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.fields.title}</h1>
{/* render post.fields.body */}
</article>
);
}Contentful Image API for OG images
Contentful's Image API can resize and crop images on the fly — perfect for ensuring your OG images are always 1200×630:
// Contentful image URL with transforms
const ogImageUrl = `https:${asset.fields.file.url}?w=1200&h=630&fit=fill&f=center&q=80&fm=jpg`;
// Parameters:
// w=1200 → width 1200px
// h=630 → height 630px
// fit=fill → crop to fill dimensions (no letterboxing)
// f=center → focus on center when cropping
// q=80 → quality 80% (reduces file size)
// fm=jpg → force JPG format
// Example output URL:
// https://images.ctfassets.net/space123/asset456/hash/photo.jpg?w=1200&h=630&fit=fillGatsby + Contentful: gatsby-head API
With Gatsby 4.19+, use the Head export to add OG tags from Contentful GraphQL data:
// src/templates/blog-post.tsx
import { graphql, HeadFC } from "gatsby";
export const query = graphql`
query BlogPost($slug: String!) {
contentfulBlogPost(slug: { eq: $slug }) {
title
excerpt
slug
ogImage {
file {
url
}
}
}
}
`;
export const Head: HeadFC = ({ data }) => {
const post = data.contentfulBlogPost;
const ogImage = post.ogImage
? `https:${post.ogImage.file.url}?w=1200&h=630&fit=fill`
: "https://yourdomain.com/og/default.png";
return (
<>
<title>{post.title}</title>
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<meta property="og:image" content={ogImage} />
<meta property="og:url" content={`https://yourdomain.com/blog/${post.slug}`} />
<meta property="og:type" content="article" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={post.title} />
<meta name="twitter:image" content={ogImage} />
</>
);
};
export default function BlogPostTemplate({ data }) {
const post = data.contentfulBlogPost;
return (
<article>
<h1>{post.title}</h1>
</article>
);
};Nuxt + Contentful: useSeoMeta
<!-- pages/blog/[slug].vue -->
<script setup>
const route = useRoute();
const { data: post } = await useAsyncData(
`post-${route.params.slug}`,
() => $fetch(`/api/contentful/post/${route.params.slug}`)
);
const ogImage = post.value?.ogImage
? `https:${post.value.ogImage.fields.file.url}?w=1200&h=630&fit=fill`
: "https://yourdomain.com/og/default.png";
useSeoMeta({
title: post.value?.title,
description: post.value?.excerpt,
ogTitle: post.value?.title,
ogDescription: post.value?.excerpt,
ogImage: ogImage,
ogUrl: `https://yourdomain.com/blog/${route.params.slug}`,
ogType: "article",
twitterCard: "summary_large_image",
twitterTitle: post.value?.title,
twitterImage: ogImage,
});
</script>
<template>
<article>
<h1>{{ post?.title }}</h1>
</article>
</template>Verify your Contentful OG tags
After deploying, paste your URL into OGFixer to preview how Twitter, Slack, Discord, and LinkedIn will render your links.