In today's tutorial, I will guide you through creating a production-ready OpenGraph image API using Next.js and Sharp. This implementation draws from my experience building Gleam.so's image generation system, which now handles thousands of requests daily.
Setting Up the API Foundation
Let's begin with the basic API route structure in Next.js 14:
// app/api/og/route.ts
import { NextResponse } from 'next/server';
import sharp from 'sharp';
import { ImageProcessor } from '../../../lib/image-processor';
export const runtime = 'edge';
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const title = searchParams.get('title');
const theme = searchParams.get('theme') || 'light';
if (!title) {
return new NextResponse('Missing title parameter', {
status: 400
});
}
const image = await generateOGImage({
title,
theme
});
return new NextResponse(image, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=31536000, immutable'
}
});
} catch (error) {
console.error('OG Generation failed:', error);
return new NextResponse('Image generation failed', {
status: 500
});
}
}
Implementing Image Processing
The image processing logic requires careful consideration of performance and quality. Here's how we can implement it:
// lib/image-processor.ts
import sharp from 'sharp';
export class ImageProcessor {
private readonly width = 1200;
private readonly height = 630;
private readonly padding = 60;
async generateOGImage(options: GenerationOptions): Promise<Buffer> {
const {
title,
theme = 'light',
template = 'default'
} = options;
// Create base image
const svg = await this.generateSVG({
title,
theme,
template
});
// Convert SVG to PNG with optimizations
return sharp(Buffer.from(svg))
.resize(this.width, this.height)
.png({
compressionLevel: 9,
quality: 80
})
.toBuffer();
}
private async generateSVG(options: SVGOptions): Promise<string> {
const { title, theme } = options;
const backgroundColor = theme === 'light' ? '#ffffff' : '#1a1a1a';
const textColor = theme === 'light' ? '#000000' : '#ffffff';
return `
<svg
width="${this.width}"
height="${this.height}"
viewBox="0 0 ${this.width} ${this.height}"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="100%" height="100%" fill="${backgroundColor}"/>
<text
x="${this.padding}"
y="${this.height / 2}"
font-family="Inter"
font-size="64"
font-weight="bold"
fill="${textColor}"
>
${this.escapeHTML(title)}
</text>
</svg>
`;
}
private escapeHTML(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
}
Implementing Error Handling
Robust error handling is crucial for a production API. Here's a comprehensive approach:
// lib/error-handler.ts
export class ErrorHandler {
async handleError(error: Error, context: ErrorContext): Promise<Buffer> {
// Log error with context
console.error('Image generation failed:', {
error,
context,
timestamp: new Date().toISOString()
});
// Generate fallback image
return this.generateFallbackImage(context);
}
private async generateFallbackImage(context: ErrorContext): Promise<Buffer> {
// Create a simple fallback image
const fallbackSVG = `
<svg width="1200" height="630">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text
x="50%"
y="50%"
text-anchor="middle"
font-family="system-ui"
font-size="48"
fill="#343a40"
>
${context.title || 'Image Unavailable'}
</text>
</svg>
`;
return sharp(Buffer.from(fallbackSVG))
.png()
.toBuffer();
}
}
Performance Optimization
To ensure optimal performance, let's implement caching and optimization strategies:
// lib/cache-manager.ts
import { Redis } from 'ioredis';
export class CacheManager {
private redis: Redis;
private readonly TTL = 7 * 24 * 60 * 60; // 1 week
constructor() {
this.redis = new Redis(process.env.REDIS_URL);
}
async getFromCache(key: string): Promise<Buffer | null> {
try {
const cached = await this.redis.get(key);
return cached ? Buffer.from(cached, 'base64') : null;
} catch (error) {
console.error('Cache fetch failed:', error);
return null;
}
}
async setInCache(key: string, image: Buffer): Promise<void> {
try {
await this.redis.set(
key,
image.toString('base64'),
'EX',
this.TTL
);
} catch (error) {
console.error('Cache set failed:', error);
}
}
}
// Implementation in API route
import { CacheManager } from '../lib/cache-manager';
const cacheManager = new CacheManager();
export async function GET(request: Request) {
const cacheKey = generateCacheKey(request.url);
// Try cache first
const cached = await cacheManager.getFromCache(cacheKey);
if (cached) {
return new NextResponse(cached, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=31536000, immutable'
}
});
}
// Generate and cache if not found
const image = await generateOGImage(/* params */);
await cacheManager.setInCache(cacheKey, image);
return new NextResponse(image, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=31536000, immutable'
}
});
}
Monitoring and Analytics
To maintain and improve the system, implement monitoring:
// lib/analytics.ts
export class Analytics {
async trackGeneration(params: {
duration: number;
success: boolean;
cached: boolean;
error?: Error;
}) {
await fetch(process.env.ANALYTICS_ENDPOINT, {
method: 'POST',
body: JSON.stringify({
event: 'og_generation',
...params,
timestamp: new Date().toISOString()
})
});
}
}
Using the API
Here's how to use the API in your Next.js application:
// Example usage in your pages
const OGImage = ({ title }: { title: string }) => {
const ogImageUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/og?title=${
encodeURIComponent(title)
}&theme=light`;
return (
<Head>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
</Head>
);
};
Performance Considerations
A few key points to ensure optimal performance:
- Edge Runtime usage for faster response times
- Aggressive caching strategy
- Image optimization with Sharp
- Error handling with fallbacks
- Monitoring for continuous improvement
Looking for a Ready Solution?
While building your own API is educational, you might want to consider using Gleam.so for a production-ready solution. It implements all these best practices and more, saving you development and maintenance time.
Questions?
Drop your questions in the comments! I'm here to help you implement this in your own projects.
Part of the Making OpenGraph Work series. Follow for more web development insights!
Top comments (0)