빠른 시작 — 첫 번째 OG 이미지 생성하기
이 FastOG 빠른 시작 가이드를 통해 몇 분 안에 첫 번째 동적 Open Graph 이미지를 배포할 수 있습니다. 가입은 무료이며, 렌더링 할당량을 위해 Dynamic API 요금제를 선택하세요. 대시보드에서 API 키와 HMAC 시크릿을 필요에 따라 생성할 수 있습니다. 모든 렌더링은 깨끗하게 제공되며, 무료 테스터를 포함한 모든 요금제에서 워터마크가 없습니다. Free OG Image Tester를 사용해 파이프라인을 직접 확인한 후, 아래 단계에 따라 자신의 URL에 서명하세요.
작동 방식 — 아키텍처
FastOG는 온디맨드 OG 이미지를 위해 설계되었습니다. 웹사이트가 페이지 렌더링 시점에 URL에 서명하고, 크롤러가 이를 가져오며, FastOG가 PNG를 한 번 렌더링한 뒤 Cloudflare가 캐시하여 이후의 모든 요청은 즉시 처리되고 비용이 들지 않습니다.
sequenceDiagram
participant You as You share a link
participant Crawler as Social crawler
participant Site as Your website
participant FastOG as FastOG API
participant Edge as Edge cache
You->>Crawler: share a link to your post
Crawler->>Site: GET the page without JavaScript
Site->>Site: sign the og:image URL at render time
Site-->>Crawler: HTML with signed og:image URL
Crawler->>Edge: fetch the og:image URL
Edge->>FastOG: cache miss, first time
FastOG->>FastOG: verify the HMAC covers the exact params
FastOG-->>Edge: render PNG, counts 1 toward monthly quota, cached for 1 year
Edge-->>Crawler: PNG
Crawler-->>You: image shown in the post preview
Note over Edge, Crawler: identical params and URL mean a cache hit with no render
"그냥 작동"하게 만드는 세 가지 규칙
- 시간이 아닌 콘텐츠에 서명하세요. 서명은 모든 파라미터(제목, 템플릿 등)를 포함합니다. 동일한 파라미터는 항상 동일한 URL을 생성합니다. 이것이 엣지 캐시가 작동하는 이유입니다. 동일한 콘텐츠 → 동일한 URL → 영구적인 캐시 히트.
- 이미지는 첫 렌더링 이후 영구적입니다. 서명은 첫 번째 렌더링만 제한합니다. 이미지가 렌더링되면 FastOG의 엣지는 콘텐츠 파라미터를 키로 하여 1년간 보관합니다. 이후 동일한 파라미터로 들어오는 요청은 엣지에서 처리되며 렌더러에 도달하지 않습니다(할당량에 포함되지 않음). 캐시된 콘텐츠에 대한 잘못된 서명은 여전히 캐시된 PNG를 반환합니다(응답은 어느 계층이 응답했는지 보고합니다:
X-FastOG-Worker-Cache: HIT또는R2). 캐시 미스일 때만 401이 반환됩니다. - 유일한 실패 모드. 변조된 URL(누군가
title/template를 변경한 경우)은 캐시 미스 시 FastOG에서 401을 받습니다. 따라서 할당량을 소진하는 데 사용될 수 없습니다. 정상적인 URL은 만료되지 않습니다. 키의 HMAC 시크릿을 교체할 때까지 유효합니다.
1. API 키와 HMAC 시크릿 받기
-
FastOG 대시보드 → API 키로 이동합니다.
-
**"새 API 키 만들기"**를 클릭합니다.
-
이름을 지정합니다(예: "프로덕션 웹사이트").
-
두 값을 모두 복사합니다(한 번만 표시됩니다!):
- API 키:
sk_...(접두사 뒤 32자) - HMAC 시크릿: 64자 16진수 문자열
- API 키:
-
환경 변수에 저장합니다:
env
# Your website backend .env
FASTOG_API_KEY=sk_your_full_api_key
FASTOG_HMAC_SECRET=a1b2c3d4e5f6... # 64 chars
FASTOG_API_URL=https://fastog.com/api/v1/og
2. URL 서명 함수 구현
다양한 프레임워크와 패턴에 대한 전체 튜토리얼은 다음을 참조하세요:
- Next.js App Router
- Next.js Pages Router
- React
- SvelteKit
- Vue / Nuxt
- Astro
- Express
- Vanilla Node.js
- JavaScript
- PHP / Laravel
- Python
- Ruby / Rails
Node.js / Next.js
javascript
// lib/fastog-signer.js
import crypto from "crypto";
// RFC-3986 encode (encodeURIComponent leaves !'()* unencoded).
const enc = (s) =>
encodeURIComponent(s).replace(
/[!'()*]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
);
export function generateOgImageUrl(params) {
const baseUrl = process.env.FASTOG_API_URL;
const apiKey = process.env.FASTOG_API_KEY;
const hmacSecret = process.env.FASTOG_HMAC_SECRET;
const all = { ...params, key: apiKey };
// Canonical: every param (except s), sorted, RFC-3986 encoded, 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("&");
// Content-signed: message = path + canonical query. No timestamp.
const signature = crypto
.createHmac("sha256", hmacSecret)
.update(`GET/api/v1/og\n${canonical}`)
.digest("hex");
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(k, vv);
}
url.searchParams.set("s", signature);
return url.toString();
}
PHP (Laravel)
php
// app/Services/QuickogSigner.php
function generateOgImageUrl(array $params): string
{
$baseUrl = config('services.fastog.api_url', 'https://fastog.com/api/v1/og');
$apiKey = config('services.fastog.api_key');
$hmacSecret = config('services.fastog.hmac_secret');
$all = array_merge($params, ['key' => $apiKey]);
// Canonical: every param except s, keys sorted (array values sorted too),
// RFC-3986 encoded, joined with '&'. Content-signed — no timestamp.
$pairs = [];
foreach ($all as $k => $v) {
$values = is_array($v) ? $v : [$v];
sort($values, SORT_STRING);
foreach ($values as $vv) {
$pairs[] = rawurlencode((string) $k) . '=' . rawurlencode((string) $vv);
}
}
sort($pairs, SORT_STRING);
$canonical = implode('&', $pairs);
$signature = hash_hmac('sha256', 'GET/api/v1/og' . "\n" . $canonical, $hmacSecret);
return $baseUrl . '?' . http_build_query(array_merge($all, ['s' => $signature]));
}
Python (Django/FastAPI)
python
# utils/fastog.py
import hmac
import hashlib
import os
from urllib.parse import quote, urlencode
def _enc(s):
return quote(str(s), safe='-._~') # RFC-3986: only unreserved chars
def generate_og_image_url(params: dict) -> str:
base_url = os.environ['FASTOG_API_URL']
api_key = os.environ['FASTOG_API_KEY']
hmac_secret = os.environ['FASTOG_HMAC_SECRET']
all_params = {**params, 'key': api_key}
# Canonical: every param except s, sorted, RFC-3986 encoded, joined with '&'.
pairs = []
for k, v in all_params.items():
for vv in sorted(v if isinstance(v, list) else [v]):
pairs.append(f"{_enc(k)}={_enc(vv)}")
canonical = '&'.join(sorted(pairs))
# Content-signed: message = path + canonical query. No timestamp.
signature = hmac.new(
hmac_secret.encode(),
f"GET/api/v1/og\n{canonical}".encode(),
hashlib.sha256,
).hexdigest()
return f"{base_url}?{urlencode({**all_params, 's': signature})}"
3. 블로그/웹사이트에서 사용하기
Next.js App Router
tsx
// app/blog/[slug]/page.tsx
import { generateOgImageUrl } from "@/lib/fastog-signer";
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
const ogImageUrl = generateOgImageUrl({
template: "blog",
title: post.title,
subtitle: post.excerpt,
author: post.author.name,
});
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [ogImageUrl],
},
};
}
Laravel Blade
blade
{{-- resources/views/blog/show.blade.php --}}
@php
$ogImageUrl = generateOgImageUrl([
'template' => 'blog',
'title' => $post->title,
'subtitle' => $post->excerpt,
'author' => $post->author->name,
]);
@endphp
<head>
<meta property="og:image" content="{{ $ogImageUrl }}" />
<meta property="og:title" content="{{ $post->title }}" />
<meta property="og:description" content="{{ $post->excerpt }}" />
</head>
Django 템플릿
django
{# blog/post_detail.html #}
{% load fastog_tags %}
<head>
<meta property="og:image" content="{% generate_og_image_url post %}" />
<meta property="og:title" content="{{ post.title }}" />
<meta property="og:description" content="{{ post.excerpt }}" />
</head>
4. 구현 테스트
bash
# Generate a test URL
node -e "
const { generateOgImageUrl } = require('./lib/fastog-signer');
console.log(generateOgImageUrl({ title: 'Test', template: 'default' }));
"
# Test with curl
curl "https://fastog.com/api/v1/og?title=Test&key=sk_quick_123abc&s=<sig>"
예상 결과:
- ✅ 유효한 서명 → OG 이미지 반환
- ❌ 변조된 매개변수(예:
title변경) →{"code": "invalid_signature"} - ❌ 잘못된 서명 →
{"code": "invalid_signature"}
5. 보안 체크리스트
- [ ] HMAC 비밀 키를 환경 변수에 저장(코드에 직접 포함하지 않음)
- [ ] 모든 요청에 HTTPS 사용
- [ ] 비밀 키를 클라이언트 측 JavaScript에 노출하지 않음
- [ ] 실패한 서명 시도 모니터링
6. 오류 처리
javascript
if (response.status === 401) {
const error = await response.json();
switch (error.code) {
case "missing_signature":
// URL is missing `key` or `s` — re-generate it with your signer.
break;
case "invalid_signature":
// A param changed after signing, or the secret doesn't match.
// Re-sign the URL after any content change; verify the secret on both sides.
break;
}
}
다음 단계
- 웹사이트 백엔드에서 서명(signing)을 구현합니다(튜토리얼 참조).
- FastOG API로 테스트합니다.
- 실패한 서명 시도에 대한 로그를 모니터링합니다.
- 반복적인 실패에 대한 알림을 설정합니다.
전체 문서는 HMAC 서명 가이드를 참조하세요.