OG Images ใน Next.js Pages Router
สร้าง OG images ใน Next.js Pages Router ด้วย Dynamic API แบบสมัครสมาชิกของ FastOG — โดยต้องมีการสมัครสมาชิก Dynamic API (สร้าง API key ของคุณในแดชบอร์ด) ใช้ Free OG Image Tester เพื่อดูตัวอย่างเทมเพลตใดก็ได้ จากนั้นเชื่อมต่อ OG images แบบไดนามิกด้วย URL ที่ลงนามด้วย HMAC ในเส้นทาง Pages Router ของคุณ
การตั้งค่า
1. สร้างยูทิลิตี้ตัวเซ็นชื่อ (signer utility)
// 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. ตัวแปรสภาพแวดล้อม (environment variables)
# .env.local
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
การสร้างเว็บไซต์แบบคงที่ (Static Site Generation - SSG)
// pages/blog/[slug].tsx
import Head from "next/head";
import { generateSignedOgUrl } from "@/lib/og-image-signer";
export default function BlogPost({ post, ogImageUrl }) {
return (
<>
<Head>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
</Head>
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
</article>
</>
);
}
export async function getStaticPaths() {
const posts = await getAllPosts();
return {
paths: posts.map((post) => ({ params: { slug: post.slug } })),
fallback: "blocking",
};
}
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
const ogImageUrl = generateSignedOgUrl({
template: "blog",
title: post.title,
subtitle: post.excerpt,
});
return {
props: {
post,
ogImageUrl,
},
revalidate: 3600,
};
}
การเรนเดอร์ฝั่งเซิร์ฟเวอร์ (Server-Side Rendering, SSR)
// pages/products/[id].tsx
import Head from "next/head";
import { generateSignedOgUrl } from "@/lib/og-image-signer";
export default function Product({ product, ogImageUrl }) {
return (
<>
<Head>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:title" content={product.name} />
</Head>
<main>
<h1>{product.name}</h1>
<p>{product.price}</p>
</main>
</>
);
}
export async function getServerSideProps({ params }) {
const product = await getProduct(params.id);
const ogImageUrl = generateSignedOgUrl({
template: "ecommerce",
title: product.name,
price: product.price,
image: product.image,
});
return {
props: {
product,
ogImageUrl,
},
};
}
ตัวพร็อกซีเส้นทาง API
// pages/api/og.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { generateSignedOgUrl } from "@/lib/og-image-signer";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { title, template = "default" } = req.query;
if (!title) {
return res.status(400).json({ error: "Missing title" });
}
const ogImageUrl = generateSignedOgUrl({
template: template as string,
title: title as string,
});
res.json({ ogImageUrl });
}
ข้อผิดพลาดที่พบบ่อย
รูปแบบข้อความที่ไม่ถูกต้อง
ข้อความที่ลงนามคือ GET/api/v1/og บวกกับอักขระขึ้นบรรทัดใหม่ (newline) บวกกับคิวรีแบบ canonical
❌ ผิด: GET/api/v1/og + path + apiKey ต่อกัน (ไม่มีคิวรีแบบ canonical)
✅ ถูก: GET/api/v1/og + \n + คู่ enc(key)=enc(value) แบบ canonical ที่เรียงลำดับแล้ว (รวมถึง key)
ลงนามในไบต์ที่แตกต่างจากที่คุณส่ง
สร้าง URL สุดท้ายจากคู่ enc(key)=enc(value) ชุดเดียวกันกับที่คุณลงนาม มิฉะนั้นเซิร์ฟเวอร์จะคืนค่า 401 invalid_signature