Understanding OG Images in React SPAs
⚠️ Important: Client-side React apps (SPAs) cannot set meta tags for social media crawlers because they execute JavaScript. Social crawlers like Twitter and Facebook don't run JavaScript — they only see your initial HTML.
For dynamic OG images in React, you need one of these approaches:
- Server-Side Rendering (SSR) — Use Next.js, Remix, or custom SSR
- Static Site Generation (SSG) — Pre-render pages at build time
- Pre-rendering service — Use services like Prerender.io
- Backend API — Generate meta tags on your server
Option 1: React with SSR Framework (Recommended)
The easiest approach is using a React framework with SSR:
- Next.js — Most popular, great DX
- Remix — Full-stack React framework
- Gatsby — Great for static sites
Remix Example
app/routes/blog.$slug.tsx
import { json, MetaFunction, LoaderFunction } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
export const loader: LoaderFunction = async ({ params }) => {
const post = await getPost(params.slug);
const ogImage = `https://ogimageapi.io/api/generate?` +
new URLSearchParams({
title: post.title,
subtitle: post.author,
theme: 'dark'
});
return json({ post, ogImage });
};
export const meta: MetaFunction = ({ data }) => {
const { post, ogImage } = data;
return [
{ title: post.title },
{ name: 'description', content: post.excerpt },
{ property: 'og:title', content: post.title },
{ property: 'og:description', content: post.excerpt },
{ property: 'og:image', content: ogImage },
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:image', content: ogImage }
];
};
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
return (
<article>
<h1>{post.title}</h1>
{/* content */}
</article>
);
}
Option 2: React-Helmet for SPAs with Pre-rendering
If you're using a pre-rendering service, use React-Helmet to set meta tags:
src/components/SEO.tsx
import { Helmet } from 'react-helmet-async';
interface SEOProps {
title: string;
description: string;
author?: string;
}
export function SEO({ title, description, author }: SEOProps) {
const ogImage = `https://ogimageapi.io/api/generate?` +
new URLSearchParams({
title,
subtitle: author || '',
theme: 'dark'
});
return (
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:type" content="article" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImage} />
</Helmet>
);
}
// Usage in a page component
function BlogPost({ post }) {
return (
<>
<SEO
title={post.title}
description={post.excerpt}
author={post.author}
/>
<article>
<h1>{post.title}</h1>
</article>
</>
);
}
Option 3: Vite SSR
For Vite projects, enable SSR for OG image support:
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
ssr: {
noExternal: ['react-helmet-async']
}
});
Build-Time Image Generation
For static sites, generate OG images at build time:
scripts/generate-og-images.ts
import fs from 'fs';
import path from 'path';
async function generateOGImages() {
const posts = await getAllPosts();
for (const post of posts) {
const response = await fetch('https://ogimageapi.io/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.OG_IMAGE_API_KEY
},
body: JSON.stringify({
title: post.title,
subtitle: post.author,
theme: 'dark'
})
});
const buffer = Buffer.from(await response.arrayBuffer());
const outputPath = path.join('public', 'og', `${post.slug}.png`);
fs.writeFileSync(outputPath, buffer);
console.log(`Generated: ${outputPath}`);
}
}
generateOGImages();