Quick Start — Generate Your First OG Image
This FastOG quick start gets your first dynamic Open Graph image live in minutes. Signing up is free — then choose a Dynamic API plan for render quota. You create your API key and HMAC secret on demand in the dashboard. Paid API renders are clean (no watermark). Try the Free OG Image Tester to see the pipeline in action, then sign your own URLs with the steps below.
How it works — Architecture
FastOG is built for on-demand OG images: your website signs a URL at page-render time, the crawler fetches it, FastOG renders the PNG once, and Cloudflare caches it so every later fetch is instant and free.
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
The three rules that make it "just work"
- Sign the content, not the clock. The signature covers every parameter (title, template, …). Identical params always produce the identical URL — that's what makes the edge cache work: same content → same URL → cache hit forever.
- The image is permanent after the first render. The signature only gates the first render. Once an image is rendered, the edge caches it for a year keyed on the content params — later requests with the same params are served from the edge and never hit FastOG (don't count against your quota). A bogus signature on cached content still returns the cached PNG (
cf-cache-status: HIT); only a cache miss gets 401'd. - The one failure mode. A tampered URL (someone changes
title/template) gets 401 at FastOG on a cache miss — so it can never be used to spend your quota. Legit URLs never expire: they stay valid until you rotate the key's HMAC secret.
1. Get Your API Key and HMAC Secret
-
Go to FastOG Dashboard → API Keys
-
Click "Create New API Key"
-
Give it a name (e.g., "Production Website")
-
Copy both values (shown only once!):
- API Key:
sk_...(32 chars after prefix) - HMAC Secret: 64-character hex string
- API Key:
-
Store them in your environment:
# 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. Implement URL Signing Function
For complete tutorials with multiple frameworks and patterns, see:
- 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
// 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)
// 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)
# 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. Use in Your Blog/Website
Next.js App Router
// 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
{{-- 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 Template
{# 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. Test Your Implementation
# 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>"
Expected:
- ✅ Valid signature → Returns OG image
- ❌ Tampered param (e.g.
titlechanged) →{"code": "invalid_signature"} - ❌ Invalid signature →
{"code": "invalid_signature"}
5. Security Checklist
- [ ] HMAC secret stored in environment variables (not in code)
- [ ] Using HTTPS for all requests
- [ ] Secret never exposed in client-side JavaScript
- [ ] Monitoring failed signature attempts
6. Handle Errors
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;
}
}
Next Steps
- Implement signing in your website backend (see the tutorials)
- Test with the FastOG API
- Monitor logs for failed signature attempts
- Set up alerting for repeated failures
For full documentation, see the HMAC Signature Guide.
Full API Reference
Related
Try it free — no signup required
Preview and test dynamic OG images in seconds.
Open the Free OG Image Tester