Astro에서 OG 이미지 생성하기
FastOG API를 사용하여 Astro에서 OG 이미지를 생성합니다. Dynamic API 구독(대시보드에서 API 키 생성)을 기반으로 하며, 구독 기반, HMAC 보안이 적용됩니다. 이 가이드에서는 HMAC 서명된 OG 이미지 URL을 생성하는 서명 유틸리티(signer utility)를 만들고 동적 Open Graph 이미지를 즉시 렌더링하는 방법을 보여줍니다. 통합 전에 무료 OG 이미지 테스터(Free OG Image Tester)를 확인하여 미리 보기를 검토하십시오.
빠른 시작
1. 서명자 유틸리티 생성
// 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. 환경 변수
# .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
블로그 게시물
---
// 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>
콘텐츠 컬렉션
// 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 };
---
// 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>
전자상거래 예시
---
// 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로 전달하세요.
---
// 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 엔드포인트
// 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 등을 지원합니다. 서명(signing)은 항상 .astro frontmatter(서버 측)에서 수행됩니다:
---
// 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>
일반적인 함정
클라이언트 측 서명
OG URL을 클라이언트 측 JavaScript에서 서명하지 마십시오.
❌ 잘못된 예:
<script>
// Runs in browser — secret exposed!
const ogUrl = generateSignedOgUrl({ title: "Test" });
</script>
✅ 올바른 예:
---
// 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을 반환합니다.