在 Astro 中生成 OG 图片
使用 FastOG API 在 Astro 中生成 OG 图片——需要 Dynamic API 订阅(在仪表盘中创建你的 API 密钥),基于订阅,HMAC 安全保护。本指南将向你展示如何创建一个签名工具(signer utility),用于生成 HMAC 签名的 OG 图片 URL,并动态渲染 Open Graph 图片。在集成之前,请使用免费的 OG 图片测试工具(Free OG Image Tester)进行预览。
快速开始
1. 创建签名工具函数
javascript
// src/lib/og-image-signer.js
import crypto from "node:crypto";
export function generateSignedOgUrl(params, options = {}) {
const {
baseUrl = "https://fastog.com/api/v1/og",
apiKey = import.meta.env.FASTOG_API_KEY,
hmacSecret = import.meta.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
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
博客文章
astro
---
// src/pages/blog/[slug].astro
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";
const post = await getPost(Astro.params.slug);
const ogImageUrl = generateSignedOgUrl({
template: "blog",
title: post.title,
subtitle: post.excerpt,
author: post.author.name,
});
---
<html>
<head>
<title>{post.title}</title>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
</head>
<body>
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
</article>
</body>
</html>
内容集合
javascript
// src/content/config.ts
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
excerpt: z.string(),
author: z.string(),
date: z.date(),
}),
});
export const collections = { blog };
astro
---
// src/pages/blog/[slug].astro
import { getCollection } from "astro:content";
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";
const post = (await getCollection("blog")).find(
(entry) => entry.slug === Astro.params.slug
);
const ogImageUrl = generateSignedOgUrl({
template: "blog",
title: post.data.title,
subtitle: post.data.excerpt,
author: post.data.author,
});
---
<html>
<head>
<title>{post.data.title}</title>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:title" content={post.data.title} />
<meta property="og:description" content={post.data.excerpt} />
</head>
<body>
<article>
<h1>{post.data.title}</h1>
<p>{post.data.excerpt}</p>
</article>
</body>
</html>
电子商务示例
astro
---
// src/pages/products/[id].astro
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";
const product = await getProduct(Astro.params.id);
const ogImageUrl = generateSignedOgUrl({
template: "ecommerce",
title: product.name,
price: product.price,
image: product.image,
});
---
<html>
<head>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:title" content={product.name} />
</head>
<body>
<h1>{product.name}</h1>
<p>{product.price}</p>
</body>
</html>
用户资料(博客模板)
FastOG 没有 profile 模板——请使用 blog 生成资料风格卡片,将用户姓名作为 title 传入,个人简介作为 subtitle 传入,姓名作为 author 传入。
astro
---
// src/pages/users/[username].astro
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";
const user = await getUser(Astro.params.username);
const ogImageUrl = generateSignedOgUrl({
template: "blog",
title: user.name,
subtitle: user.bio,
author: user.name,
});
---
<html>
<head>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:title" content={`${user.name} - Profile`} />
</head>
<body>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</body>
</html>
API 端点
javascript
// src/pages/api/og.js
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";
export async function GET({ request }) {
const url = new URL(request.url);
const title = url.searchParams.get("title");
const template = url.searchParams.get("template") || "default";
if (!title) {
return new Response(JSON.stringify({ error: "Missing title" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const ogImageUrl = generateSignedOgUrl({
template,
title,
});
return new Response(JSON.stringify({ ogImageUrl }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
框架组件
Astro 支持 React、Vue、Svelte 等框架。签名操作始终在 .astro frontmatter(服务端)中进行:
astro
---
// src/pages/blog/[slug].astro
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";
import BlogPost from "../../components/BlogPost.svelte";
const post = await getPost(Astro.params.slug);
const ogImageUrl = generateSignedOgUrl({
template: "blog",
title: post.title,
});
---
<html>
<head>
<meta property="og:image" content={ogImageUrl} />
</head>
<body>
<BlogPost client:load {post} />
</body>
</html>
常见陷阱
客户端签名
切勿在客户端 JavaScript 中签署 OG URL。
❌ 错误:
astro
<script>
// Runs in browser — secret exposed!
const ogUrl = generateSignedOgUrl({ title: "Test" });
</script>
✅ 正确:
astro
---
// Runs at build/SSR time — secret safe
const ogImageUrl = generateSignedOgUrl({ title: post.title });
---
签名的字节与发送的内容不一致
签名覆盖 GET/api/v1/og 加上换行符再加上规范查询——即除 s 之外所有参数的排序后的 enc(key)=enc(value) 键值对,包括 key。请使用这些相同的编码键值对构建最终 URL,否则服务器将返回 401 invalid_signature。