Engineering NotebookTech

Dynamic OG Images at the Edge: Per-Page Social Cards Without a Designer

Every page and article deserves its own on-brand share card. Here is how we generate one for each route at request time, rendered on Vercel, with no designer in the loop.

JS
J Supreme Studio
Engineering · May 22, 2026 · 9 min read
Sharer/Y𝕏inf

A link is shared more often than it is read. The first thing anyone sees of your page is not the page — it is the card that unfurls in WhatsApp, in a group chat, in an X post. Most service sites ship one generic card for the whole site. We generate a unique one for every route, at request time, and never open a design tool to do it.

The Open Graph image is the rectangle that appears when a URL is pasted into Messenger, Slack, LinkedIn, iMessage, or an X post. Get it right and a shared link looks like a product. Get it wrong — or leave it blank — and your link looks like every other untitled blue rectangle on the internet. For a studio shipping 30+ platforms, doing this by hand once per page was never going to happen. So we made it code.

Why most service sites skip this — and lose

The default failure mode is one static og-image.png set in the root layout. Every page — home, pricing, each blog post, each product — shares the same picture. The title in the card never matches the page. It works, technically. It just throws away the single most-seen pixel of your whole site.

  • It is the highest-traffic image you own — seen by everyone who considers clicking, not just those who do.
  • It is a free trust signal — a card that names the actual article reads as a real publication, not a parked domain.
  • It compounds — every share, every repost, every link in a DM carries it for years.
  • Nobody maintains it — a designer-made card per page is a job nobody schedules, so it never ships.

The image route in Next.js 16

Next.js ships a primitive built exactly for this: a file convention that turns a route into an image. You drop an opengraph-image.tsx next to a page, export a component that returns JSX, and the framework renders it to a PNG using Satori — a library that turns a constrained subset of HTML and CSS into SVG, then rasterizes it. No headless browser, no canvas math. You write a div and you get a picture.

tsx
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
import { getPost } from '@/lib/blog';

export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function Image({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return new ImageResponse(
    (
      <div style={{
        width: '100%', height: '100%', display: 'flex',
        flexDirection: 'column', justifyContent: 'space-between',
        padding: 64, background: '#0a0a0a', color: '#fafafa',
      }}>
        <div style={{ fontSize: 22, letterSpacing: 4, opacity: 0.6 }}>
          {post.kicker?.toUpperCase()}
        </div>
        <div style={{ fontSize: 64, fontWeight: 700, lineHeight: 1.05 }}>
          {post.title}
        </div>
        <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 24 }}>
          <span>{post.author}</span>
          <span>jsupremetech.online</span>
        </div>
      </div>
    ),
    { ...size }
  );
}

That is the whole mechanism. The route reads the post, lays out a card with the real title and kicker, and returns it. Next.js wires the resulting URL into the page's <meta property="og:image"> automatically — you do not hand-write the meta tag. Paste the URL anywhere and the unfurl shows the right card.

Fonts and layout you actually control

System fonts make every card look like a system. Our identity is pure black-and-white minimalism — Space Grotesk for display, JetBrains Mono for the eyebrow. To get those into the card you load the font file as an ArrayBuffer and hand it to ImageResponse. The font is fetched once, cached, and reused for every render.

tsx
// load the brand font once, pass it to every card
const display = await fetch(
  new URL('./SpaceGrotesk-Bold.ttf', import.meta.url)
).then((r) => r.arrayBuffer());

return new ImageResponse(<Card post={post} />, {
  ...size,
  fonts: [{ name: 'Space Grotesk', data: display, weight: 700, style: 'normal' }],
});

With the font in hand, the layout is just a constrained flex column: an eyebrow at the top, the title filling the middle, a byline and domain pinned to the bottom. One template, driven by data. Swap the title and the byline and you have a different card — same grid, same ink, same monogram in the corner. That is the trick that lets one file serve an entire publication and every product page.

Fig 1 — one template, many cards: kicker, title, and byline are slots; the mono grid and ink scale never move

Truncation is a real design decision

Titles vary in length and Satori will not auto-shrink text to fit. Decide up front: clamp the title to a fixed number of lines with WebkitLineClamp, or scale the font size by title length. We clamp to three lines and accept that a very long headline loses its tail — better a clean card than an overflowing one. The title in the JSON is written to fit, which keeps the system honest at authoring time.

Caching so cards stay fast

Generating an image per request sounds expensive. It is not, because you only generate it the first time. Open Graph images are immutable for a given page version, so they cache beautifully. On Vercel, the rendered PNG sits in the edge cache and serves instantly on every subsequent unfurl. The render only re-runs when the content changes or the cache expires.

ts
// the route is statically rendered at build for known slugs;
// generateStaticParams pre-bakes every post's card
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((p) => ({ slug: p.slug }));
}

// for dynamic pages, cache the response at the edge
export const revalidate = 86400; // re-render at most once a day

The result: the crawler that fetches your card — Facebook's scraper, Slack's unfurler, X's bot — gets a cached PNG in well under a second. No browser spins up. No queue. The first visitor pays the render cost once; everyone after gets the cache.

1200x630
card dimensions
1
template file
30+
platforms, one system

One template, the whole studio

This is where it ties back to how we build. The same monochrome OG template that renders cards for The Signal renders them for every platform we ship — BP Couriers, The Mover Guy, Unique Solid Surfaces, NEXPRO. Each gets its own per-page cards in its own brand pack, driven by the same routine. Set the ink scale and the wordmark once per project; every page inherits a correct card for free. It is the same one-engine-many-packs idea behind Supreme Suite, applied to the share card.

If the card is just markup, the most consistent design system is the one that renders it — not the one that draws it by hand.

Studio principle

A designer making one card is a nice afternoon. A designer making a card for every page of 30+ platforms is a job that never ends and therefore never starts. Code does it on every deploy, for free, forever — and it never drifts from the brand, because the brand is the template.

Want every page of your site to share like a product instead of a blank rectangle?

Start a project
#next.js#open-graph#edge#seo#typescript

Questions, answered

Do I need a separate design tool for OG images?
No. The whole point of the image route is that the card is a React component rendered to PNG at request time. You write a layout once with your brand fonts and colors, and every page gets a correct card automatically — no Figma, no Photoshop, no per-page export.
Will generating an image per page slow my site down?
No, because the image is cached. The PNG is immutable for a given page version, so it renders once and serves from the edge cache after that. Known routes are pre-baked at build, so most cards never render on demand at all.
Why not just use one static OG image for the whole site?
Because the card is the most-seen pixel you own — everyone who considers clicking sees it, not just those who click. A card that names the actual page reads as a real product. A generic one reads as a parked domain. It is a free conversion lever most sites leave on the table.
Recommended · affiliate

The stack we actually use

The tools below run our own systems and the ones we build for clients. A few are affiliate links — they support The Signal at no cost to you.

Some links on this site are affiliate links. If you click through and sign up or buy, J Supreme Tech may earn a commission at no extra cost to you. We only recommend tools we actually use to build and run our own systems. Full disclosure.

Found the signal useful? Pass it on.

Sharer/Y𝕏inf
The Communications Debrief

Get the signal in your inbox.

Field notes on building software, marketing systems and the markets — what we shipped, the problems and the fixes. One clean dispatch, no noise.

Double opt-in. No spam, unsubscribe anytime.