Files
blog-nextjs/app/api/og/route.tsx
gbanyan d7dc279d32 Add dynamic OG image generation for social media sharing
## Features
- Create /api/og route for dynamic Open Graph image generation
- Beautiful gradient design with site branding
- Display post title, description, and tags
- Support for both light and dark themes
- Proper sizing for social media (1200x630)

## Implementation
- Use @vercel/og package for image generation
- Add OpenGraph and Twitter Card metadata to blog posts
- Fallback to localhost for development
- Uses NEXT_PUBLIC_SITE_URL environment variable for production

## Social Media Support
- OpenGraph (Facebook, LinkedIn, etc.)
- Twitter Cards with large image preview
- Article metadata including publish time and tags

Example usage:
/api/og?title=Post+Title&description=Post+Desc&tags=tag1,tag2,tag3

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 14:55:36 +08:00

167 lines
4.5 KiB
TypeScript

import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
// Get parameters
const title = searchParams.get('title') || 'Blog Post';
const description = searchParams.get('description') || '';
const tags = searchParams.get('tags')?.split(',').slice(0, 3) || [];
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'space-between',
backgroundColor: '#0f172a',
backgroundImage: 'radial-gradient(circle at 25px 25px, #1e293b 2%, transparent 0%), radial-gradient(circle at 75px 75px, #1e293b 2%, transparent 0%)',
backgroundSize: '100px 100px',
padding: '80px',
}}
>
{/* Header with gradient */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '20px',
}}
>
<div
style={{
width: '8px',
height: '60px',
background: 'linear-gradient(135deg, #3b82f6, #60a5fa)',
borderRadius: '4px',
}}
/>
<div
style={{
fontSize: '32px',
fontWeight: 600,
color: '#f8fafc',
letterSpacing: '-0.02em',
}}
>
</div>
</div>
{/* Main content */}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '24px',
maxWidth: '900px',
}}
>
{/* Title */}
<div
style={{
fontSize: '72px',
fontWeight: 700,
color: '#f8fafc',
lineHeight: 1.1,
letterSpacing: '-0.03em',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{title}
</div>
{/* Description */}
{description && (
<div
style={{
fontSize: '28px',
color: '#cbd5e1',
lineHeight: 1.4,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{description}
</div>
)}
{/* Tags */}
{tags.length > 0 && (
<div
style={{
display: 'flex',
gap: '12px',
flexWrap: 'wrap',
}}
>
{tags.map((tag, i) => (
<div
key={i}
style={{
backgroundColor: '#1e293b',
color: '#94a3b8',
padding: '8px 20px',
borderRadius: '20px',
fontSize: '20px',
border: '1px solid #334155',
}}
>
#{tag.trim()}
</div>
))}
</div>
)}
</div>
{/* Footer with accent line */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
width: '100%',
}}
>
<div
style={{
flex: 1,
height: '2px',
background: 'linear-gradient(90deg, #3b82f6, transparent)',
}}
/>
<div
style={{
fontSize: '24px',
color: '#64748b',
}}
>
gbanyan.net
</div>
</div>
</div>
),
{
width: 1200,
height: 630,
}
);
} catch (e: any) {
console.error('Error generating OG image:', e);
return new Response(`Failed to generate image: ${e.message}`, {
status: 500,
});
}
}