OG Images in JavaScript
Generate og:image in JavaScript with the FastOG API — sign up free for 100 credits (create your API key in the dashboard), credit-based, HMAC-secured. This guide shows you how to create a signer utility for HMAC-signed OG image URLs and render dynamic Open Graph images on the fly. Try the Free OG Image Tester to preview before you ship.
Table of Contents
Quick Start
Goal: Generate a signed OG image URL in 5 minutes.Step 1: Install Dependencies
npm install crypto # Built-in, no install neededStep 2: Create Signer Utility
// lib/og-image-signer.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;
if (!apiKey || !hmacSecret) {
throw new Error("Missing FASTOG_API_KEY or FASTOG_HMAC_SECRET");
}
const timestamp = Math.floor(Date.now() / 1000);
const url = new URL(baseUrl);
// Add content params
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
// apiKey is the full key(sk_...) from FASTOG_API_KEY
// Compute signature
const path = url.pathname;
const message = `GET${path}${timestamp}${apiKey}`;
const signature = crypto
.createHmac("sha256", hmacSecret)
.update(message)
.digest("hex");
// Add auth params
url.searchParams.set("key", apiKey);
url.searchParams.set("s", signature);
url.searchParams.set("t", timestamp);
return url.toString();
}Step 3: Use in Your App
// app/blog/[slug]/page.jsx
import { generateSignedOgUrl } from "@/lib/og-image-signer";
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
const ogImageUrl = generateSignedOgUrl({
template: "blog",
title: post.title,
subtitle: post.excerpt,
});
return {
openGraph: {
images: [ogImageUrl],
},
};
}Step 4: Test
node -e "
const { generateSignedOgUrl } = require('./lib/og-image-signer');
console.log(generateSignedOgUrl({ title: 'Test' }));
"Next.js App Router
Pattern 1: Metadata API (Recommended)
// app/blog/[slug]/page.tsx
import { generateSignedOgUrl } from "@/lib/og-image-signer";
import { getPost } from "@/lib/posts";
interface PageProps {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({ params }: PageProps) {
const { slug } = await params;
const post = await getPost(slug);
const ogImageUrl = generateSignedOgUrl({
template: "blog",
title: post.title,
subtitle: post.excerpt,
author: post.author.name,
// Optional: customize colors
theme: "indigo",
});
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [
{
url: ogImageUrl,
width: 1200,
height: 630,
alt: post.title,
},
],
type: "article",
},
twitter: {
card: "summary_large_image",
images: [ogImageUrl],
},
};
}Pattern 2: API Route Proxy
Use this if you want to cache OG images or add custom logic.
// app/api/og/route.ts
import { NextRequest, NextResponse } from "next/server";
import { generateSignedOgUrl } from "@/lib/og-image-signer";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const title = searchParams.get("title");
const template = searchParams.get("template") || "default";
if (!title) {
return NextResponse.json(
{ error: "Missing title parameter" },
{ status: 400 },
);
}
// Generate signed URL
const ogImageUrl = generateSignedOgUrl({
template,
title,
});
// Option 1: Redirect to FastOG
return NextResponse.redirect(ogImageUrl);
// Option 2: Fetch and return image(for caching)
// const response = await fetch(ogImageUrl);
// const imageBuffer = await response.arrayBuffer();
// return new NextResponse(imageBuffer, {
// headers: {
// 'Content-Type': 'image/png',
// 'Cache-Control': 'public, max-age=31536000, immutable',
// },
// });
} catch (error) {
console.error("OG image generation failed:", error);
return NextResponse.json(
{ error: "Failed to generate OG image" },
{ status: 500 },
);
}
}Usage:
// In your component
<meta property="og:image" content={`/api/og?title=${encodeURIComponent(post.title)}`} />Pattern 3: Server Component Direct
// app/blog/[slug]/page.tsx
import { generateSignedOgUrl } from '@/lib/og-image-signer';
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
const ogImageUrl = generateSignedOgUrl({
template: 'blog',
title: post.title,
});
return (
<article>
<head>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:title" content={post.title} />
</head>
{/* ... post content ... */}
</article>
);
}Next.js Pages Router
// pages/blog/[slug].tsx
import { generateSignedOgUrl } from '@/lib/og-image-signer';
import { GetStaticProps, GetStaticPaths } from 'next';
interface BlogPostProps {
post: Post;
ogImageUrl: string;
}
export default function BlogPost({ post, ogImageUrl }: BlogPostProps) {
return (
<>
<Head>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
</Head>
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
</article>
</>
);
}
export async function getStaticPaths() {
// Generate paths...
}
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
const ogImageUrl = generateSignedOgUrl({
template: 'blog',
title: post.title,
subtitle: post.excerpt,
});
return {
props: {
post,
ogImageUrl,
},
};
}Express.js Backend
Setup
npm install express cryptoImplementation
// app.js
import express from "express";
import crypto from "crypto";
import { URL } from "url";
const app = express();
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;
const timestamp = Math.floor(Date.now() / 1000);
const url = new URL(baseUrl);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
const apiKey = apiKey;
const path = url.pathname;
const message = `GET${path}${timestamp}${apiKey}`;
const signature = crypto
.createHmac("sha256", hmacSecret)
.update(message)
.digest("hex");
url.searchParams.set("key", apiKey);
url.searchParams.set("s", signature);
url.searchParams.set("t", timestamp);
return url.toString();
}
// Blog post route
app.get("/blog/:slug", async (req, res) => {
const post = await getPost(req.params.slug);
const ogImageUrl = generateSignedOgUrl({
template: "blog",
title: post.title,
subtitle: post.excerpt,
});
res.render("blog/post", {
post,
ogImageUrl,
});
});
// API endpoint for dynamic OG images
app.get("/api/og", (req, res) => {
const { title, template = "default" } = req.query;
if (!title) {
return res.status(400).json({ error: "Missing title" });
}
const ogImageUrl = generateSignedOgUrl({
template,
title,
});
res.json({ ogImageUrl });
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});EJS Template
<!-- views/blog/post.ejs -->
<!DOCTYPE html>
<html>
<head>
<meta property="og:image" content="<%= ogImageUrl %>" />
<meta property="og:title" content="<%= post.title %>" />
<meta property="og:description" content="<%= post.excerpt %>" />
</head>
<body>
<h1><%= post.title %></h1>
</body>
</html>Vanilla Node.js
// og-signer.js
const crypto = require("crypto");
const { URL } = require("url");
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;
const timestamp = Math.floor(Date.now() / 1000);
const url = new URL(baseUrl);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
const apiKey = apiKey;
const path = url.pathname;
const message = `GET${path}${timestamp}${apiKey}`;
const signature = crypto
.createHmac("sha256", hmacSecret)
.update(message)
.digest("hex");
url.searchParams.set("key", apiKey);
url.searchParams.set("s", signature);
url.searchParams.set("t", timestamp);
return url.toString();
}
module.exports = { generateSignedOgUrl };Usage:
// index.js
const { generateSignedOgUrl } = require("./og-signer");
const ogUrl = generateSignedOgUrl({
template: "blog",
title: "My Blog Post",
});
console.log("OG Image URL:", ogUrl);Frontend Integration Patterns
Pattern 1: Static Site Generation (SSG)
Best for: Blog posts, product pages, content that doesn't change frequently.
// Generate at build time
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
openGraph: {
images: [generateSignedOgUrl({ title: post.title })],
},
};
}Pattern 2: Server-Side Rendering (SSR)
Best for: Dynamic content, user-specific pages.
export default async function Page({ params }) {
const data = await fetchData(params.id);
const ogImageUrl = generateSignedOgUrl({ title: data.title });
return (
<>
<Head>
<meta property="og:image" content={ogImageUrl} />
</Head>
<Content data={data} />
</>
);
}Pattern 3: Client-Side Fallback
When server-side generation isn't possible.
'use client';
import { useEffect, useState } from 'react';
export default function ShareButtons({ post }) {
const [ogImageUrl, setOgImageUrl] = useState<string | null>(null);
useEffect(() => {
// Fetch from your API that generates signed URLs
fetch(`/api/og?title=${encodeURIComponent(post.title)}`)
.then(res => res.json())
.then(data => setOgImageUrl(data.ogImageUrl));
}, [post.title]);
const shareUrl = ogImageUrl || '/fallback-og.png';
return (
<div>
<button onClick={() => shareToTwitter(shareUrl)}>
Share on Twitter
</button>
</div>
);
}⚠️ Warning: Client-side generation means social crawlers won't see the dynamic OG image. Use SSR/SSG when possible.
Testing & Debugging
Unit Tests
// __tests__/og-image-signer.test.js
import { generateSignedOgUrl } from "../lib/og-image-signer";
describe("generateSignedOgUrl", () => {
const mockParams = {
title: "Test Post",
template: "blog",
};
const mockOptions = {
apiKey: "sk_test1234567890",
hmacSecret: "a".repeat(64),
};
it("generates a valid URL", () => {
const url = generateSignedOgUrl(mockParams, mockOptions);
expect(url).toMatch(/^https:\/\/fastog\.dev\/api\/v1\/og\?/);
expect(url).toContain("key=sk_test");
expect(url).toContain("s=");
expect(url).toContain("t=");
});
it("includes all params", () => {
const url = generateSignedOgUrl(mockParams, mockOptions);
const parsed = new URL(url);
expect(parsed.searchParams.get("title")).toBe("Test Post");
expect(parsed.searchParams.get("template")).toBe("blog");
});
it("throws without API key", () => {
expect(() => generateSignedOgUrl(mockParams, {})).toThrow();
});
});Manual Testing
# 1. Generate URL
node -e "
const { generateSignedOgUrl } = require('./lib/og-image-signer');
const url = generateSignedOgUrl({ title: 'Test' });
console.log(url);
"
# 2. Test with curl
curl "https://fastog.com/api/v1/og?title=Test&key=sk_quick_123abc&s=<sig>&t=<ts>"
# 3. Check response headers
curl -I "https://fastog.com/api/v1/og?title=Test&key=sk_quick_123abc&s=<sig>&t=<ts>"Debug Mode
function generateSignedOgUrl(params, options = {}) {
const debug = options.debug || process.env.DEBUG_OG === "true";
// ... existing code ...
if (debug) {
console.log("OG URL Debug:", {
baseUrl,
path,
timestamp,
apiKey,
message,
signature,
finalUrl: url.toString(),
});
}
return url.toString();
}Common Pitfalls
❌ Wrong:
const apiKey = apiKey; // "sk_abc123..."✅ Correct:
const apiKey = apiKey; // "sk_abc12"2. Timestamp in Milliseconds
❌ Wrong:
const timestamp = Date.now(); // 1722718800000 (milliseconds)✅ Correct:
const timestamp = Math.floor(Date.now() / 1000); // 1722718800 (seconds)3. Wrong Message Format
❌ Wrong:
const message = `${method}:${path}:${timestamp}:${apiKey}`; // "GET:/api/v1/og:1722718800:sk_abc12"✅ Correct:
const message = `${method}${path}${timestamp}${apiKey}`; // "GET/api/v1/og1722718800sk_abc12"4. Exposing HMAC Secret in Client Code
❌ Wrong:
// In client component
const ogUrl = generateSignedOgUrl(params, {
hmacSecret: process.env.NEXT_PUBLIC_HMAC_SECRET,
});✅ Correct:
// In server component or API route
export async function generateMetadata() {
const ogUrl = generateSignedOgUrl(params);
// ...
}5. Not Handling URL Encoding
❌ Wrong:
const url = `${baseUrl}?title=${params.title}&key=${apiKey}`;✅ Correct:
const url = new URL(baseUrl);
url.searchParams.set("title", params.title);
url.searchParams.set("key", apiKey);6. Clock Skew Issues
If your server clock is off, signatures will be rejected.
# Check server time
date
# Sync with NTP
sudo ntpdate -s time.nist.gov7. Caching Issues
Social networks cache OG images. To force refresh:
// Add cache-busting param(won't affect signature)
const url = new URL(generateSignedOgUrl(params));
url.searchParams.set("v", Date.now().toString());Environment Variables
# .env.local
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
FASTOG_BASE_URL=https://fastog.com/api/v1/og// lib/env.ts
import { z } from "zod";
const envSchema = z.object({
FASTOG_API_KEY: z.string().startsWith("sk_"),
FASTOG_HMAC_SECRET: z.string().length(64),
FASTOG_BASE_URL: z
.string()
.url()
.optional()
.default("https://fastog.com/api/v1/og"),
});
export const env = envSchema.parse(process.env);Next Steps
Full API Reference
Related
Try it free — no signup required
Preview and test dynamic OG images in seconds.
Open the Free OG Image Tester