OG images in Next.js
Render OG images outside your app with ogmake: no fonts to bundle, no emoji pipeline, no
Flexbox-only limits, and the same URL works from any stack. The pattern is one function
that builds a signed URL and one line in
generateMetadata.
1. Get a key
Sign in at /login and create a key in the dashboard. The signing secret
is shown once. Put both in .env.local, never in client code:
OGMAKE_KEY_ID=k_... OGMAKE_SIGNING_SECRET=base64-secret-shown-once
Still on a hand-issued beta key? Email support@ogmake.com.
2. Sign a URL on the server
The signature is an HMAC-SHA256 of the sorted, encoded query (the signing page has the exact rules). In Node it is a few lines, so drop this helper into your app:
// lib/ogmake.ts — server-only
import { createHmac } from "node:crypto";
const enc = (v: string) =>
encodeURIComponent(v).replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
export function ogImageUrl(params: Record<string, string>): string {
const all: Record<string, string> = { template: "blog", ...params };
const canonical = Object.keys(all)
.sort()
.map((k) => enc(k) + "=" + enc(all[k]))
.join("&");
const sig = createHmac("sha256", Buffer.from(process.env.OGMAKE_SIGNING_SECRET!, "base64"))
.update(canonical)
.digest("base64url");
return "https://ogmake.com/i/" + process.env.OGMAKE_KEY_ID + "/" + sig + "?" + canonical;
} 3. Use it in generateMetadata
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { ogImageUrl } from "@/lib/ogmake";
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost((await params).slug);
const image = ogImageUrl({ title: post.title, author: post.author, site: "My Blog" });
return {
title: post.title,
openGraph: { images: [{ url: image, width: 1200, height: 630 }] },
twitter: { card: "summary_large_image", images: [image] },
};
} The image renders the first time a crawler requests the URL and is cached afterwards, so nothing is rendered in your build or at request time. Because the URL changes whenever the title changes, a stale preview after an edit is a platform cache problem, not a stale image: see og:image not showing.
Fields per template are on the reference page. If you would rather not sign at all for static pages, a public URL works with just the key id.
What ogmake renders
- Full CSS layouts. Cards render in a real browser engine, so CSS grid, flexbox and stacking with
z-indexall work in a template. - Any script. Chinese, Japanese, Korean, Arabic and Hebrew text use bundled Noto fonts, and emoji render in color.
- No runtime in your app. The image is rendered on ogmake's side and served from a URL, so there are no fonts to bundle, no edge function to size and no image route to maintain.
- An editor for the design. Build and preview a card in the editor (see the editor guide) before you wire it to your pages.
- One signed URL. Everything above comes from the single URL that
ogImageUrl()builds.
Moving from an ImageResponse route? See
migrating from @vercel/og. Once your tags are
live, confirm them with the OG image checker.