หัวข้อ

สร้าง OG Images ใน Next.js — Dynamic og:image API

สร้าง OG images ใน Next.js ด้วย Dynamic API แบบสมัครสมาชิกของ FastOG — ด้วยการสมัครสมาชิก Dynamic API (สร้าง API key ของคุณในแดชบอร์ด) และ Free OG Image Tester จะแสดงตัวอย่างภาพของคุณก่อนที่คุณจะเขียนโค้ดแม้แต่บรรทัดเดียว คู่มือนี้จะแสดงวิธีสร้าง Open Graph images แบบไดนามิกด้วย URL ที่ลงนามด้วย HMAC โดยตรงจาก Next.js App Router

เริ่มต้นใช้งานอย่างรวดเร็ว

1. สร้างยูทิลิตี้สำหรับลงนาม (signer utility)

javascript
// lib/og-image-signer.js
import crypto from "crypto";

export function generateSignedOgUrl(params, options = {}) {
    const {
        baseUrl = "https://fastog.com/api/v1/og",
        apiKey = process.env.FASTOG_API_KEY,
        hmacSecret = process.env.FASTOG_HMAC_SECRET,
    } = options;

    if (!apiKey || !hmacSecret) {
        throw new Error("Missing FASTOG_API_KEY or FASTOG_HMAC_SECRET");
    }

    // RFC-3986 percent-encoding: keep A-Za-z0-9-._~
    const enc = (s) =>
        encodeURIComponent(s).replace(
            /[!'()*]/g,
            (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
        );

    // Every query param except the signature, INCLUDING key
    const all = { ...params, key: apiKey };

    // Canonical query: sorted enc(key)=enc(value) pairs joined with &
    const pairs = [];
    for (const [k, v] of Object.entries(all)) {
        for (const vv of Array.isArray(v) ? v : [v]) {
            pairs.push(`${enc(k)}=${enc(vv)}`);
        }
    }
    pairs.sort();
    const canonical = pairs.join("&");

    // Sign the exact bytes the server will verify
    const signature = crypto
        .createHmac("sha256", hmacSecret)
        .update(`GET/api/v1/og\n${canonical}`)
        .digest("hex");

    // Build the URL from the same pairs so bytes match the canonical form
    const url = new URL(baseUrl);
    for (const [k, v] of Object.entries(all)) {
        for (const vv of Array.isArray(v) ? v : [v]) {
            url.searchParams.append(Array.isArray(v) ? `${k}[]` : k, vv);
        }
    }
    url.searchParams.set("s", signature);

    return url.toString();
}

2. ตั้งค่าตัวแปรสภาพแวดล้อม

bash
# .env.local
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
typescript
// app/blog/[slug]/page.tsx
import { generateSignedOgUrl } from "@/lib/og-image-signer";
import { getPost } from "@/lib/posts";

interface PageProps {
    params: Promise<{ slug: string }>;
}

export async function generateMetadata({ params }: PageProps) {
    const { slug } = await params;
    const post = await getPost(slug);

    const ogImageUrl = generateSignedOgUrl({
        template: "blog",
        title: post.title,
        subtitle: post.excerpt,
        author: post.author.name,
    });

    return {
        title: post.title,
        description: post.excerpt,
        openGraph: {
            title: post.title,
            description: post.excerpt,
            images: [
                {
                    url: ogImageUrl,
                    width: 1200,
                    height: 630,
                    alt: post.title,
                },
            ],
            type: "article",
        },
        twitter: {
            card: "summary_large_image",
            images: [ogImageUrl],
        },
    };
}

export default async function BlogPost({ params }: PageProps) {
    const { slug } = await params;
    const post = await getPost(slug);

    return (
        <article>
            <h1>{post.title}</h1>
            <p>{post.excerpt}</p>
        </article>
    );
}

รูปแบบที่ 2: ตัวพร็อกซีเส้นทาง API

ใช้รูปแบบนี้หากคุณต้องการแคชรูปภาพ OG หรือเพิ่มตรรกะที่กำหนดเอง

typescript
// app/api/og/route.ts
import { NextRequest, NextResponse } from "next/server";
import { generateSignedOgUrl } from "@/lib/og-image-signer";

export async function GET(request: NextRequest) {
    try {
        const { searchParams } = new URL(request.url);
        const title = searchParams.get("title");
        const template = searchParams.get("template") || "default";

        if (!title) {
            return NextResponse.json(
                { error: "Missing title parameter" },
                { status: 400 },
            );
        }

        const ogImageUrl = generateSignedOgUrl({
            template,
            title,
        });

        // Option 1: Redirect to FastOG
        return NextResponse.redirect(ogImageUrl);

        // Option 2: Fetch and return image (for caching)
        // const response = await fetch(ogImageUrl);
        // const imageBuffer = await response.arrayBuffer();
        // return new NextResponse(imageBuffer, {
        //     headers: {
        //         'Content-Type': 'image/png',
        //         'Cache-Control': 'public, max-age=31536000, immutable',
        //     },
        // });
    } catch (error) {
        console.error("OG image generation failed:", error);
        return NextResponse.json(
            { error: "Failed to generate OG image" },
            { status: 500 },
        );
    }
}

การใช้งานในเทมเพลต:

typescript
<meta property="og:image" content={`/api/og?title=${encodeURIComponent(post.title)}`} />

รูปแบบที่ 3: Server Component โดยตรง

typescript
// app/blog/[slug]/page.tsx
import { generateSignedOgUrl } from '@/lib/og-image-signer';

export default async function BlogPost({ params }) {
    const post = await getPost(params.slug);

    const ogImageUrl = generateSignedOgUrl({
        template: 'blog',
        title: post.title,
    });

    return (
        <article>
            <head>
                <meta property="og:image" content={ogImageUrl} />
                <meta property="og:title" content={post.title} />
            </head>
            {/* ... post content ... */}
        </article>
    );
}

ตัวอย่างอีคอมเมิร์ซ

typescript
// app/products/[id]/page.tsx
import { generateSignedOgUrl } from "@/lib/og-image-signer";

export async function generateMetadata({ params }) {
    const { id } = await params;
    const product = await getProduct(id);

    const ogImageUrl = generateSignedOgUrl({
        template: "ecommerce",
        title: product.name,
        price: product.price,
        image: product.image,
    });

    return {
        openGraph: {
            title: product.name,
            description: product.description,
            images: [{ url: ogImageUrl, width: 1200, height: 630 }],
            type: "product",
        },
    };
}

โปรไฟล์ผู้ใช้ (เทมเพลตบล็อก)

FastOG ไม่มีเทมเพลต profile — ให้ใช้ blog สำหรับการ์ดสไตล์โปรไฟล์ โดยส่งชื่อผู้ใช้เป็น title, ประวัติย่อเป็น subtitle และชื่อเป็น author

typescript
// app/users/[username]/page.tsx
import { generateSignedOgUrl } from "@/lib/og-image-signer";

export async function generateMetadata({ params }) {
    const { username } = await params;
    const user = await getUser(username);

    const ogImageUrl = generateSignedOgUrl({
        template: "blog",
        title: user.name,
        subtitle: user.bio,
        author: user.name,
    });

    return {
        openGraph: {
            title: `${user.name} - Profile`,
            description: user.bio,
            images: [{ url: ogImageUrl, width: 1200, height: 630 }],
            type: "profile",
        },
    };
}

ข้อผิดพลาดที่พบบ่อย

การลงนามไบต์ที่แตกต่างจากที่คุณส่ง

ลายเซ็นครอบคลุม GET/api/v1/og บวกกับอักขระขึ้นบรรทัดใหม่ และคิวรีแบบ canonical — คู่ enc(key)=enc(value) ที่เรียงลำดับของพารามิเตอร์ทั้งหมดยกเว้น s รวมถึง key ด้วย สร้าง URL สุดท้ายจากคู่ที่เข้ารหัสชุดเดียวกันนั้น มิฉะนั้นเซิร์ฟเวอร์จะคืนค่า 401 invalid_signature

การเปิดเผย HMAC Secret ในโค้ดฝั่งไคลเอนต์

❌ ผิด:

javascript
// In client component
const ogUrl = generateSignedOgUrl(params, {
    hmacSecret: process.env.NEXT_PUBLIC_HMAC_SECRET,
});

✅ ถูกต้อง:

javascript
// In server component or API route (never expose to client)
export async function generateMetadata() {
    const ogUrl = generateSignedOgUrl(params);
}

เอกสารอ้างอิง API ฉบับเต็ม

ลองใช้ฟรี — ไม่ต้องสมัครสมาชิก

ดูตัวอย่างและทดสอบภาพ OG แบบไดนามิกได้ในไม่กี่วินาที

เปิดตัวทดสอบภาพ OG ฟรี

A BeCaSimulations site