Tạo OG Image cho Ứng dụng React
Tạo og:image bằng React thông qua FastOG API — với gói đăng ký Dynamic API (tạo API key của bạn trong dashboard), dựa trên đăng ký, bảo mật bằng HMAC. Hướng dẫn này chỉ cho bạn cách tạo một tiện ích ký (signer utility) cho các URL OG image được ký bằng HMAC và hiển thị hình ảnh Open Graph động ngay lập tức. Dùng Free OG Image Tester để xem trước thiết kế của bạn trước khi phát hành.
Bắt đầu nhanh
1. Tạo tiện ích ký (signer utility)
// src/utils/og-image-signer.js
export async function generateSignedOgUrl(params, options = {}) {
const {
baseUrl = "https://fastog.com/api/v1/og",
apiKey = import.meta.env.VITE_FASTOG_API_KEY ||
process.env.REACT_APP_FASTOG_API_KEY,
hmacSecret = import.meta.env.VITE_FASTOG_HMAC_SECRET ||
process.env.REACT_APP_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 (Web Crypto HMAC-SHA256)
const signingKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(hmacSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const signature = Array.from(
new Uint8Array(
await crypto.subtle.sign(
"HMAC",
signingKey,
new TextEncoder().encode(`GET/api/v1/og\n${canonical}`),
),
),
)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
// 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();
}
⚠️ Quan trọng: Trong các SPA React, việc ký diễn ra ở phía máy khách (client-side). Đối với môi trường sản xuất, hãy chuyển việc ký sang một route API ở phía máy chủ (backend) để giữ bí mật HMAC được an toàn.
Route API Backend (Khuyến nghị)
Backend Express
// server/routes/og.js
import express from "express";
import crypto from "crypto";
const router = express.Router();
router.get("/api/og", (req, res) => {
const { title, template = "default", ...params } = req.query;
if (!title) {
return res.status(400).json({ error: "Missing title" });
}
const apiKey = process.env.FASTOG_API_KEY;
const hmacSecret = process.env.FASTOG_HMAC_SECRET;
const enc = (s) =>
encodeURIComponent(s).replace(
/[!'()*]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
);
const all = { template, title, ...params, key: apiKey };
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("&");
const signature = crypto
.createHmac("sha256", hmacSecret)
.update(`GET/api/v1/og\n${canonical}`)
.digest("hex");
const url = new URL("https://fastog.com/api/v1/og");
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);
res.json({ ogImageUrl: url.toString() });
});
export default router;
Component React
// src/components/OgImage.jsx
import { useState, useEffect } from "react";
export function useOgImage(params) {
const [ogImageUrl, setOgImageUrl] = useState(null);
useEffect(() => {
const searchParams = new URLSearchParams(params);
fetch(`/api/og?${searchParams}`)
.then((res) => res.json())
.then((data) => setOgImageUrl(data.ogImageUrl))
.catch((err) => console.error("Failed to generate OG image:", err));
}, [JSON.stringify(params)]);
return ogImageUrl;
}
Ví dụ về Bài đăng Blog
// src/pages/BlogPost.jsx
import { useEffect, useState } from "react";
import { useOgImage } from "../components/OgImage";
export default function BlogPost({ post }) {
const ogImageUrl = useOgImage({
title: post.title,
template: "blog",
subtitle: post.excerpt,
});
useEffect(() => {
const ogImage = document.querySelector('meta[property="og:image"]');
if (ogImage && ogImageUrl) {
ogImage.setAttribute("content", ogImageUrl);
}
}, [ogImageUrl]);
return (
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
</article>
);
}
React Helmet (SSR)
// src/pages/BlogPost.jsx
import { Helmet } from "react-helmet-async";
import { useOgImage } from "../components/OgImage";
export default function BlogPost({ post }) {
const ogImageUrl = useOgImage({
title: post.title,
template: "blog",
subtitle: post.excerpt,
});
return (
<>
<Helmet>
<meta
property="og:image"
content={ogImageUrl || "/fallback-og.png"}
/>
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
</Helmet>
<article>
<h1>{post.title}</h1>
</article>
</>
);
}
Next.js (React Framework)
Đối với Next.js, hãy xem các hướng dẫn chuyên dụng Next.js App Router và Next.js Pages Router.
Remix
// app/routes/blog.$slug.jsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { generateSignedOgUrl } from "~/lib/og-image-signer.server";
export async function loader({ params }) {
const post = await getPost(params.slug);
const ogImageUrl = generateSignedOgUrl({
template: "blog",
title: post.title,
subtitle: post.excerpt,
});
return json({ post, ogImageUrl });
}
export function meta({ data }) {
return [
{ title: data.post.title },
{ property: "og:image", content: data.ogImageUrl },
{ property: "og:title", content: data.post.title },
];
}
export default function BlogPost() {
const { post } = useLoaderData();
return (
<article>
<h1>{post.title}</h1>
</article>
);
}
Trình ký phía máy chủ (Node.js)
// lib/og-image-signer.server.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;
// 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();
}
Biến Môi Trường
# .env (Vite)
VITE_FASTOG_API_KEY=sk_your_full_api_key_here
VITE_FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
# .env (Create React App)
REACT_APP_FASTOG_API_KEY=sk_your_full_api_key_here
REACT_APP_FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
# Server .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
Các Lỗi Thường Gặp
Lộ Bí Mật HMAC Trong Mã Phía Máy Khách
❌ Sai:
// In browser code
const ogUrl = generateSignedOgUrl(params, {
hmacSecret: import.meta.env.VITE_FASTOG_HMAC_SECRET,
});
✅ Đúng:
// Call your backend API
const res = await fetch(`/api/og?title=${post.title}`);
const { ogImageUrl } = await res.json();
Không Cập Nhật Thẻ Meta
Các trình thu thập dữ liệu mạng xã hội cần thẻ meta trong HTML ban đầu. Hãy sử dụng SSR (Next.js, Remix) hoặc proxy phía máy chủ để hỗ trợ hình ảnh OG đúng cách.