OG Images in Astro

Generate OG images in Astro 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. Check the Free OG Image Tester to preview before you integrate.

Quick Start

1. Create the signer utility

javascript
// 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");
    }

    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();
}

2. Environment variables

bash
# .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here

Blog Post

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

Content Collections

javascript
// 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 };
astro
---
// 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>

E-commerce Example

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

User Profile Example

astro
---
// src/pages/users/[username].astro
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";

const user = await getUser(Astro.params.username);

const ogImageUrl = generateSignedOgUrl({
    template: "profile",
    name: user.name,
    avatar: user.avatarUrl,
    bio: user.bio,
});
---

<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 Endpoint

javascript
// 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" },
    });
}

Framework Components

Astro supports React, Vue, Svelte, and more. The signing always happens in the .astro frontmatter (server-side):

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

Common Pitfalls

Client-side Signing

Never sign OG URLs in client-side JavaScript.

Wrong:

astro
<script>
    // Runs in browser — secret exposed!
    const ogUrl = generateSignedOgUrl({ title: "Test" });
</script>

Correct:

astro
---
// Runs at build/SSR time — secret safe
const ogImageUrl = generateSignedOgUrl({ title: post.title });
---

Timestamp in Milliseconds

Wrong: Date.now()1722718800000

Correct: Math.floor(Date.now() / 1000)1722718800

Full API Reference

Related

Try it free — no signup required

Preview and test dynamic OG images in seconds.

Open the Free OG Image Tester