← All posts

Edge runtime

Satori + Next.js OG images — the 3 errors that cost me 4 hours

Posted July 15, 2026 · 7 min read

Every Next.js SaaS needs Open Graph images — they're the preview cards when someone shares your URL on Twitter, Slack, iMessage, or LinkedIn. The Vercel docs show you ImageResponse + Satoriand say "it just works." It doesn't. Here are the 3 errors that bit me at 2am, with the fix for each.

Error 1: app/api/og/route.tsx returns 405

The Vercel docs suggest creating OG images as API routes. They don't tell you that route.tsxunder app/api/ is treated as an API route handler — and API routes only respond to GET if you export a GET function. ImageResponse doesn't ship as a GET handler correctly.

Fix: use a route segment file convention, not an API route. Place at app/og/opengraph-image.tsx or app/[param]/opengraph-image.tsx. Next.js recognizes the special filename and serves it as a static image at /og/....

Error 2: searchParams: Promise<...> 500s in Satori

In Next.js 15, page params and searchParams became async. You await them in pages and route segments. But Satori's ImageResponse doesn't await them — it expects the values synchronously. Wait, no — the problem is the opposite. If you don't await them in Satori, you pass an unresolved Promise to JSX, which throws a non-obvious 500 when Satori tries to read string properties.

Fix: use dynamic route segments with params (synchronous), not query strings. The route file app/og/[param]/opengraph-image.tsx receives params as a plain object (synchronous in edge runtime).

// app/og/[param]/opengraph-image.tsx
import { ImageResponse } from 'next/og';

export const runtime = 'edge';
export const alt = 'Open Graph image';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default function Image({ params }: { params: { param: string } }) {
  return new ImageResponse(
    <div style={{
      width: '100%', height: '100%',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      background: '#0a0e14', color: '#fff', fontSize: 48,
    }}>
      {params.param}
    </div>
  );
}

Error 3: @supabase/supabase-js doesn't fit in Edge runtime

The biggest OG image I wanted to render pulled user data from Supabase. The official @supabase/supabase-jspackage is too big — it adds 300KB+ to your edge bundle and exceeds the 1MB edge function limit when combined with Satori's font assets. Build fails with cryptic "module not found" or 500 errors at runtime.

Fix: use direct fetch against Supabase's REST API instead of the SDK. Edge runtime has native fetch; you don't need the SDK.

const res = await fetch(
  `${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/profiles?username=eq.${username}&select=name,bio`,
  {
    headers: {
      apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
      authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`,
    },
  }
);
const [profile] = await res.json();

Save this as a helper, use it in every dynamic OG image, and the bundle stays under 200KB.

Bonus: serving fonts in Edge

Satori needs fonts. If you import a font file at the top of your edge route file, Next.js bundles it — but some font files exceed edge memory limits. Two fixes:

  1. Use Google Fonts CDN instead of bundling — fetch('https://fonts.googleapis.com/...')returns the font, Satori accepts a Buffer directly.
  2. Use the same font for everything — pick one font and reuse it via shared helper. Saves edge memory.

Per-page OG images with metadata

Once you have the dynamic OG route set up, link it from each page's metadata:

// app/[username]/page.tsx
export async function generateMetadata({ params }) {
  return {
    title: `${params.username} on FitForge`,
    openGraph: {
      images: [`/og/${params.username}`],   // → edge route renders dynamically
    },
  };
}

When someone shares yourdomain.com/alice, the link preview pulls from your dynamic OG route, fetches Alice's profile, and renders their name. Looks intentional every time.

The complete OG image system (per-user, per-launch, with Supabase fetch pattern + metadata linking) is in Forge Stack. Tested on production traffic to FitForge.

Tags: #nextjs #satori #ogimage #edge #social