Skip to content

SEO Engineering

Dynamic OG Images — How Velocity X Generates Per-Post Social Cards at Build Time

One Satori Render Per Post. No Runtime. No Third-Party APIs.

🖼️

Every blog post needs an OG image for Twitter, LinkedIn, and Slack previews. Most sites either hand-export one per post (→ bottleneck) or use a generic branded template (→ invisible in feeds). Velocity X does something smarter: it auto-generates a unique social card for every post using Satori at build time. Title + emoji cover + brand palette injected into a React component, rendered to PNG, deployed with the site. One file, zero runtime cost.

Why Build-Time OG Images Win

Vercel's @vercel/og runs OG generation at request time — every time someone shares a link, the server re-renders the image. That's expensive. Satori (the headless renderer under the hood) is fast, but Cloudflare CDN caching is limited. If you're on a serverless platform, every hit is a cold function start.

Velocity X flips the model: render once at build, ship static PNG. No Lambda invocation, no Cloudflare cache tier, no API latency. The preview loads 10× faster on Slack because it's a 40KB static file, not a 300ms network call.

The Architecture: Frontmatter → Satori → PNG Asset

Build time workflow: the blog post loader reads frontmatter (title, emoji, category), passes it to a Satori component, renders to buffer, writes PNG to the static assets folder. Each post gets og-<slug>.png in public/og/. Open Graph meta tag points to /og/og-<slug>.png.

The Satori component is a React render target — it's not a server component or a page. It's a pure function that takes props (title, emoji, color palette from brand.json) and returns JSX. Satori evaluates it to Skia canvas, exports PNG. All happens during npm run build.

Code: The Satori Render Pipeline

Here's the essence:

// lib/og-image.ts
import { ImageResponse } from '@vercel/og';

export async function generateOGImage(
  title: string,
  emoji: string,
  brandColor: string
) {
  return new ImageResponse(
    (
      <div
        style={{
          width: '1200px',
          height: '630px',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          background: `linear-gradient(135deg, ${brandColor}20, ...)`
        }}
      >
        <span style={{ fontSize: '120px' }}>{emoji}</span>
        <h1 style={{ fontSize: '56px', color: '#fff' }}>{title}</h1>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

In the blog loader, call generateOGImage() for each post and write the buffer to public/og/. The HTML head tag references /og/og-<slug>.png. When Twitter crawls the page, it gets a real, fast image.

Why Manual Export Loses

Hand-exporting one OG image per post doesn't scale. After 50 posts, you've spent 5 hours in Figma exporting PNGs. Update brand colors? Re-export all 50. Velocity X's automation wins on ROI: define the template once, generate 169 social cards without manual work.

Generic site OG (same image for every post) is worse. It's invisible in Twitter feeds because every link has the same thumbnail. Velocity X's per-post cards stand out because they're unique, on-brand, and carry the post's emoji hook.

The Catch

Satori has limits: no full CSS support, no background images (only gradients + solid colors), no custom fonts beyond system fonts. For Velocity X's use case (emoji + title + gradient), these limits don't matter. If you need photographic OG images, you'll fall back to either manual export or runtime generation.

The Bottom Line

Social card generation at build time is the move. One render per blog post at deploy time, ship static PNG, zero runtime cost. The margin: 40KB static file loads in 100ms on Slack instead of a 300ms cold Lambda start. Scale to 500 posts and that's 2 minutes of social-preview latency eliminated across all your shares. Build once, ship fast.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.