Next.js guide

Next.js OG image generator

OGKit is the hosted path for Next.js Open Graph images: absolute HTTPS URLs from generateMetadata, no opengraph-image.tsx, no Satori fonts, no Edge bundle budget.

1) Per-route generateMetadata (primary pattern)

Build the image URL on the server from route data. Pass the same absolute URL to Open Graph and Twitter. Keep OGKIT_KEY in server env only.

// app/blog/[slug]/page.tsx
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPost(params.slug);
  const image = new URL("https://www.webmorp.art/api/og/article");
  image.searchParams.set("key", process.env.OGKIT_KEY!);
  image.searchParams.set("title", post.title);
  image.searchParams.set("subtitle", post.excerpt ?? "Blog");
  const url = image.toString();

  return {
    title: post.title,
    openGraph: {
      title: post.title,
      images: [{ url, width: 1200, height: 630, alt: post.title }],
    },
    twitter: { card: "summary_large_image", title: post.title, images: [url] },
  };
}

2) Shared helper (DRY across layouts)

Extract a tiny server helper so docs, changelog, and marketing pages share one contract.

// lib/ogkit.ts — server-only
export function ogkitArticle(fields: { title: string; subtitle?: string }) {
  const image = new URL("https://www.webmorp.art/api/og/article");
  image.searchParams.set("key", process.env.OGKIT_KEY!);
  image.searchParams.set("title", fields.title);
  if (fields.subtitle) image.searchParams.set("subtitle", fields.subtitle);
  return image.toString();
}

// usage in generateMetadata
const url = ogkitArticle({ title: post.title, subtitle: post.excerpt });

3) Static marketing page (build-time URL)

For a single landing page you can use a module-scope URL. Still keep the key server-side — this file must not be imported from a Client Component.

// app/pricing/page.tsx
const ogImage = new URL("https://www.webmorp.art/api/og/brand");
ogImage.searchParams.set("key", process.env.OGKIT_KEY!);
ogImage.searchParams.set("title", "Acme Pricing");
ogImage.searchParams.set("tagline", "Simple plans");

export const metadata = {
  openGraph: { images: [{ url: ogImage.toString(), width: 1200, height: 630 }] },
  twitter: { card: "summary_large_image" as const, images: [ogImage.toString()] },
};

Typical Next.js mistakes

  • Relative og:image paths (scrapers resolve against the wrong host)
  • Putting OGKIT_KEY in NEXT_PUBLIC_ or client bundles
  • Static metadata export that never updates per slug
  • Shipping opengraph-image.tsx and OGKit URL for the same route (last write wins / confusion)
  • Expecting Slack/Facebook to drop cache without a new image URL

Wrong — client / public env

// ❌ leaks key + crawlers may miss tags
"use client";
const url = `${process.env.NEXT_PUBLIC_OGKIT}/api/og/...`;

Right — server metadata

// ✅ generateMetadata / Server Component only
image.searchParams.set("key", process.env.OGKIT_KEY!);

Rescrape after deploys

Next.js metadata changes only help after HTML is live and platforms refetch og:image. Deterministic OGKit URLs cache hard — change title/subtitle (or add v=) when the card must update.

  1. Deploy and curl the page — confirm absolute og:image / twitter:image match.
  2. curl -sI the image URL — expect image/png.
  3. Facebook Sharing Debugger → Scrape Again; LinkedIn Post Inspector.
  4. Slack: reshare with a new image URL if the old unfurl sticks.

Full matrix: Caching & rescrape. Symptom → fix: Troubleshooting.

Why not always opengraph-image.tsx?

Custom ImageResponse routes win for pixel-perfect JSX. They also make you own fonts, Edge budgets, and render failures. OGKit wins when the job is a reliable 1200×630 card from page fields. Compare: OGKit vs @vercel/og.

Production checklist

  • Build URLs in generateMetadata / Server Components only.
  • Keep OGKIT_KEY out of client components and NEXT_PUBLIC_*.
  • Use absolute HTTPS URLs with width/height when possible.
  • Mirror the same URL in openGraph.images and twitter.images.
  • After title changes, update the OGKit query (or v=) and rescrape.

Related Next.js SEO pages

Frequently asked questions

How do I add dynamic Open Graph images to Next.js App Router?

Build an absolute OGKit image URL inside generateMetadata (or a server helper), then assign it to metadata.openGraph.images and twitter.images. Use params/slug data so each route gets a unique card.

Can OGKit replace a custom opengraph-image.tsx route?

Yes, when you want hosted templates and stable image URLs instead of maintaining a custom Satori route, font loading, and renderer debugging. Prefer one approach per route — do not mix both.

Where should I keep the OGKit API key?

Keep the API key in server-side environment variables (OGKIT_KEY). Never NEXT_PUBLIC_OGKIT_KEY or client components.

Why is Slack/Facebook still showing the old Next.js card?

Unfurl caches stick to the image URL. Change the OGKit query (title/subtitle or v=), redeploy metadata, then rescrape. See /guides/troubleshooting and /guides/caching-and-rescrape.

Related reading

OGKit turns one HTTPS URL into a 1200×630 Open Graph image. Read the API reference, deep guides, the Open Graph SEO guide, try the Playground, or sign in to create API keys.