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 (5 minutes)
  • Next.js App Router
  • Next.js Pages Router
  • Express.js Backend
  • Vanilla Node.js
  • Frontend Integration Patterns
  • Testing & Debugging
  • Common Pitfalls

  • Quick Start

    Goal: Generate a signed OG image URL in 5 minutes.

    Step 1: Install Dependencies

    bash
    npm install crypto  # Built-in, no install needed

    Step 2: Create Signer Utility

    javascript
    // 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

    javascript
    // 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

    bash
    node -e "
    const { generateSignedOgUrl } = require('./lib/og-image-signer');
    console.log(generateSignedOgUrl({ title: 'Test' }));
    "

    Next.js App Router

    Pattern 1: Metadata API (Recommended)

    typescript
    // 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.

    typescript
    // 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:

    typescript
    // In your component
    <meta property="og:image" content={`/api/og?title=${encodeURIComponent(post.title)}`} />

    Pattern 3: Server Component Direct

    typescript
    // 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

    typescript
    // 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

    bash
    npm install express crypto

    Implementation

    javascript
    // 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

    ejs
    <!-- 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

    javascript
    // 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:

    javascript
    // 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.

    typescript
    // 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.

    typescript
    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.

    typescript
    '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

    javascript
    // __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

    bash
    # 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

    javascript
    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:

    javascript
    const apiKey = apiKey; // "sk_abc123..."

    Correct:

    javascript
    const apiKey = apiKey; // "sk_abc12"

    2. Timestamp in Milliseconds

    Wrong:

    javascript
    const timestamp = Date.now(); // 1722718800000 (milliseconds)

    Correct:

    javascript
    const timestamp = Math.floor(Date.now() / 1000); // 1722718800 (seconds)

    3. Wrong Message Format

    Wrong:

    javascript
    const message = `${method}:${path}:${timestamp}:${apiKey}`; // "GET:/api/v1/og:1722718800:sk_abc12"

    Correct:

    javascript
    const message = `${method}${path}${timestamp}${apiKey}`; // "GET/api/v1/og1722718800sk_abc12"

    4. Exposing HMAC Secret in Client Code

    Wrong:

    javascript
    // In client component
    const ogUrl = generateSignedOgUrl(params, {
        hmacSecret: process.env.NEXT_PUBLIC_HMAC_SECRET,
    });

    Correct:

    javascript
    // In server component or API route
    export async function generateMetadata() {
        const ogUrl = generateSignedOgUrl(params);
        // ...
    }

    5. Not Handling URL Encoding

    Wrong:

    javascript
    const url = `${baseUrl}?title=${params.title}&key=${apiKey}`;

    Correct:

    javascript
    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.

    bash
    # Check server time
    date
    
    # Sync with NTP
    sudo ntpdate -s time.nist.gov

    7. Caching Issues

    Social networks cache OG images. To force refresh:

    javascript
    // Add cache-busting param(won't affect signature)
    const url = new URL(generateSignedOgUrl(params));
    url.searchParams.set("v", Date.now().toString());

    Environment Variables

    bash
    # .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
    typescript
    // 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