Appearance
React Integration
Generate OG images for React applications (Create React App, Vite, etc.)
Overview
React SPAs need server-side OG image generation for social sharing. Options:
- Build-time generation — Generate images during build
- Server/serverless function — Generate on-demand
- Signed URLs — Direct links with no server needed
Build-Time Generation
Best for content that doesn't change often.
Setup
bash
npm install node-fetchBuild Script
javascript
// scripts/generate-og.js
const fs = require('fs');
const path = require('path');
const pages = [
{ slug: 'home', title: 'Welcome to MyApp', subtitle: 'Build amazing things' },
{ slug: 'about', title: 'About Us', subtitle: 'Our mission and team' },
{ slug: 'contact', title: 'Contact', subtitle: 'Get in touch' }
];
async function generateOGImages() {
const outputDir = path.join(__dirname, '../public/og');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
for (const page of pages) {
const response = await fetch('https://ogimageapi.io/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.REACT_APP_OG_IMAGE_API_KEY
},
body: JSON.stringify({
title: page.title,
subtitle: page.subtitle,
template: 'default',
theme: 'dark'
})
});
const buffer = await response.arrayBuffer();
fs.writeFileSync(
path.join(outputDir, `${page.slug}.png`),
Buffer.from(buffer)
);
console.log(`✓ Generated ${page.slug}.png`);
}
}
generateOGImages().catch(console.error);Package.json
json
{
"scripts": {
"generate-og": "node scripts/generate-og.js",
"build": "npm run generate-og && react-scripts build"
}
}Using react-helmet
javascript
// components/SEO.jsx
import { Helmet } from 'react-helmet';
export function SEO({ title, description, image }) {
const ogImage = image || `/og/default.png`;
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="website" />
<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
javascript
// pages/BlogPost.jsx
import { SEO } from '../components/SEO';
export function BlogPost({ post }) {
return (
<>
<SEO
title={post.title}
description={post.excerpt}
image={`/og/blog-${post.slug}.png`}
/>
<article>
{/* Post content */}
</article>
</>
);
}Serverless Function (AWS Lambda)
For dynamic content, use a serverless function.
Lambda Handler
javascript
// lambda/og-image.js
const https = require('https');
exports.handler = async (event) => {
const { title, subtitle, template } = event.queryStringParameters || {};
const requestBody = JSON.stringify({
title: title || 'My App',
subtitle: subtitle || '',
template: template || 'default',
theme: 'dark'
});
return new Promise((resolve, reject) => {
const req = https.request({
hostname: 'ogimageapi.io',
path: '/api/generate',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.OG_IMAGE_API_KEY,
'Content-Length': Buffer.byteLength(requestBody)
}
}, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const buffer = Buffer.concat(chunks);
resolve({
statusCode: 200,
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=86400'
},
body: buffer.toString('base64'),
isBase64Encoded: true
});
});
});
req.on('error', reject);
req.write(requestBody);
req.end();
});
};Cloudflare Workers
javascript
// workers/og-image.js
export default {
async fetch(request, env) {
const url = new URL(request.url);
const title = url.searchParams.get('title') || 'My App';
const subtitle = url.searchParams.get('subtitle') || '';
const response = await fetch('https://ogimageapi.io/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': env.OG_IMAGE_API_KEY
},
body: JSON.stringify({
title,
subtitle,
template: 'default',
theme: 'dark'
})
});
return new Response(await response.arrayBuffer(), {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=86400'
}
});
}
};Using Signed URLs
Simplest approach — no server needed.
javascript
// utils/og.js
export async function getSignedOGUrl(params) {
const queryString = new URLSearchParams(params).toString();
const response = await fetch(
`https://ogimageapi.io/api/signed/generate?${queryString}`,
{
headers: {
'X-API-Key': process.env.REACT_APP_OG_IMAGE_API_KEY
}
}
);
const data = await response.json();
return data.url;
}javascript
// During build or SSR
const ogUrl = await getSignedOGUrl({
title: 'My Page Title',
subtitle: 'Description here',
template: 'default',
expires_in: 604800 // 7 days
});
// Use in meta tags
<meta property="og:image" content={ogUrl} />Vite Configuration
javascript
// vite.config.js
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [react()],
define: {
'process.env.OG_IMAGE_API_KEY': JSON.stringify(env.OG_IMAGE_API_KEY)
}
};
});Best Practices
- Never expose API key in client code — Use build scripts or serverless
- Generate at build time when possible
- Cache aggressively — Images rarely change
- Use signed URLs for simplest setup
- Have fallback images for errors