Zero-Click Social Cards: Automatic OG images from MDX frontmatter

August 11, 2025 (2d ago)

I was either shipping posts with bland link previews or hand-designing images I didn't have time for. The data I needed was already there — title, summary, date — sitting in MDX frontmatter. The fix: a tiny endpoint that turns that frontmatter into a good-looking OG image automatically.

Result: push a post → deploy → share the link → solid preview every time. No Figma. No copy/paste.


How it works (tiny)

  1. Expose post data (slug → title, summary, publishedAt).
  2. Add a small /api/og/[slug] route to render a 1200×630 image on demand.
  3. Point og:image to that route in your post page <head>.

That's it. Here's the minimal code.

1) Post index (whatever you already have)

// app/db/blog.ts
export type Metadata = {
  title: string;
  publishedAt: string;
  summary: string;
  image?: string;
};

export function getBlogPosts() {
  return getMDXData(path.join(process.cwd(), 'content'));
}

2) The image route

// app/api/og/[slug]/route.ts
import { ImageResponse } from "next/server";
import { getBlogPosts } from "@/app/db/blog";

// Edge runtime = fast cold starts for tiny images
export const runtime = "edge";

export async function GET(_: Request, { params }: { params: { slug: string } }) {
  const posts = getBlogPosts();
  const post = posts.find(p => p.slug === params.slug);
  
  if (!post) return new Response("Not found", { status: 404 });

  const { metadata } = post;

  return new ImageResponse(
    (
      <div
        style={{
          width: 1200,
          height: 630,
          display: "flex",
          flexDirection: "column",
          background: "#0b1220",
          color: "#e5e7eb",
          padding: 64,
          gap: 28
        }}
      >
        <div style={{ fontSize: 56, lineHeight: 1.05, letterSpacing: -0.5 }}>
          {metadata.title}
        </div>
        <div style={{ fontSize: 28, opacity: 0.9, maxWidth: 960 }}>
          {metadata.summary}
        </div>
        <div style={{ marginTop: "auto", fontSize: 24, opacity: 0.8 }}>
          {new Date(metadata.publishedAt).toLocaleDateString("en-GB", { 
            day: "2-digit", 
            month: "short", 
            year: "numeric" 
          })}
        </div>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

Tip: If you want branded typography, embed a font: fetch the .ttf, pass it in fonts: [{ name, data, weight }].

3) Wire it into <head>

// app/blog/[slug]/page.tsx (using Next.js 13+ Metadata API)
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata | undefined> {
  const { slug } = await params;
  let post = getBlogPosts().find((post) => post.slug === slug);
  if (!post) {
    return;
  }

  let {
    title,
    publishedAt: publishedTime,
    summary: description,
    image,
  } = post.metadata;
  let ogImage = image
    ? `https://ainsworth.dev${image}`
    : `https://ainsworth.dev/api/og/${post.slug}`;

  return {
    title,
    description,
    openGraph: {
      title,
      description,
      type: 'article',
      publishedTime,
      url: `https://ainsworth.dev/blog/${post.slug}`,
      images: [{ url: ogImage }],
    },
    twitter: {
      card: 'summary_large_image',
      title,
      description,
      images: [ogImage],
    },
  };
}

Design tips that make it look "done"

  • Big title, small summary. Titles wrap; summaries don't dominate.
  • One color, not a rainbow. A dark background with light text is consistent and legible.
  • Clamp long titles. If you write essay-length titles, reduce font-size slightly after ~70 chars.
  • Emoji are fine. If they look off, switch to Twemoji SVGs.

Your turn (5 minutes)

  • Add the route, render a plain card, and point og:image at it.
  • Share a post to Slack/Twitter — you should see a clean preview.
  • If you like it, add a brand font and logo dot next.

In an enterprise production environment I would pre-generate PNGs at build or set a long cache header — but you don't need either to get great previews today.

© Sam Ainsworth 2024 - 2025. All Rights Reserved.
Zero-Click Social Cards: Automatic OG images from MDX frontmatter | Sam Ainsworth