使用 Next.js 和 Sharp 构建 OpenGraph 图像 API

在今天的教程中,我将指导您使用 Next.js 和 Sharp 创建可用于生产的 OpenGraph 图像 API。此实现借鉴了我构建 Gleam.so 图像生成系统的经验,该系统现在每天处理数千个请求。

建立 API 基础

让我们从 Next.js 14 中的基本 API 路由结构开始:

// 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 
    });
  }
}

实现图像处理

图片处理逻辑需要仔细考虑性能和质量。我们可以这样实现它:

// 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 {
    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 {
    const { title, theme } = options;
    const backgroundColor = theme === 'light' ? '#ffffff' : '#1a1a1a';
    const textColor = theme === 'light' ? '#000000' : '#ffffff';

    return `
      
        
        
          ${this.escapeHTML(title)}
        
      
    `;
  }

  private escapeHTML(text: string): string {
    return text
      .replace(/&/g, '&')
      .replace(//g, '>')
      .replace(/"/g, '"')
      .replace(/'/g, ''');
  }
}

实现错误处理

对于生产 API 来说,强大的错误处理至关重要。以下是一种全面的方法:

// lib/error-handler.ts
export class ErrorHandler {
  async handleError(error: Error, context: ErrorContext): Promise {
    // 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 {
    // Create a simple fallback image
    const fallbackSVG = `
      
        
        
          ${context.title || 'Image Unavailable'}
        
      
    `;

    return sharp(Buffer.from(fallbackSVG))
      .png()
      .toBuffer();
  }
}

性能优化

为了确保最佳性能,让我们实现缓存和优化策略:

// 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 {
    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 {
    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'
    }
  });
}

监控和分析

为了维护和改进系统,实施监控:

// 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()
      })
    });
  }
}

使用 API

以下是在 Next.js 应用程序中使用 API 的方法:

// 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 (
    
      
      
      
    
  );
};

性能注意事项

确保最佳性能的几个关键点:

  • 使用 Edge Runtime 可缩短响应时间
  • 积极的缓存策略
  • 使用 Sharp 进行图像优化
  • 使用 fallback 进行错误处理
  • 监控持续改进
  • 正在寻找现成的解决方案?

    虽然构建自己的 API 很有教育意义,但您可能希望考虑使用 Gleam.so 来获得可用于生产的解决方案。它实现了所有这些最佳实践以及更多,从而节省了您的开发和维护时间。

    问题?

    在评论中提出你的问题!我在这里帮助你在自己的项目中实现这一点。