MDX Open Graph Images: Auto-Generate OG Images from Frontmatter

How to auto-generate og:image for MDX blog posts using Next.js, Contentlayer, and Velite — with frontmatter-driven metadata and dynamic edge images.

The MDX + OG image workflow

MDX-based blogs are common in the Next.js ecosystem — used in documentation sites, personal blogs, and company blogs. The challenge is that each MDX file is essentially a standalone piece of content without an automatic social preview image.

The solution is to use frontmatter fields from your MDX files to drive OG metadata generation, and then either reference a static image or generate a dynamic one using Next.js's edge image generation.

MDX frontmatter for OG tags

Define the fields you need in your MDX frontmatter:

---
title: "How We Cut Our Load Time by 60%"
description: "We rewrote our data fetching layer and cut page load time by 60%. Here's what we learned."
date: "2026-03-01"
author: "Jane Smith"
image: "/og-images/load-time-post.png"   # optional: static OG image
tags: ["performance", "react", "nextjs"]
---

# How We Cut Our Load Time by 60%

Content starts here...

The image field is optional — if present, use it as the OG image. If absent, auto-generate one from the title.

Option 1: Velite (modern, type-safe)

Velite is the modern replacement for Contentlayer with active maintenance. Define your schema:

// velite.config.ts
import { defineConfig, defineCollection, s } from "velite";

const posts = defineCollection({
  name: "Post",
  pattern: "content/posts/**/*.mdx",
  schema: s
    .object({
      title: s.string(),
      description: s.string(),
      date: s.isodate(),
      author: s.string().optional(),
      image: s.string().optional(),
      slug: s.slug("posts"),
    })
    .transform((data) => ({
      ...data,
      // Auto-generate OG image URL if not provided
      ogImage: data.image || `/api/og?title=${encodeURIComponent(data.title)}`,
    })),
});

export default defineConfig({ collections: { posts } });
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { posts } from ".velite";

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = posts.find((p) => p.slug === params.slug);
  
  if (!post) return { title: "Not Found" };
  
  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      type: "article",
      publishedTime: post.date,
      images: [{ url: post.ogImage, width: 1200, height: 630 }],
    },
    twitter: {
      card: "summary_large_image",
      title: post.title,
      description: post.description,
      images: [post.ogImage],
    },
  };
}

Option 2: next-mdx-remote with gray-matter

// lib/mdx.ts
import fs from "fs";
import path from "path";
import matter from "gray-matter";

const postsDir = path.join(process.cwd(), "content/posts");

export function getPost(slug: string) {
  const fullPath = path.join(postsDir, `${slug}.mdx`);
  const raw = fs.readFileSync(fullPath, "utf8");
  const { data, content } = matter(raw);
  
  return {
    slug,
    title: data.title as string,
    description: data.description as string,
    date: data.date as string,
    image: data.image as string | undefined,
    content,
  };
}

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

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = getPost(params.slug);
  
  const ogImage =
    post.image ||
    `/api/og?title=${encodeURIComponent(post.title)}`;
  
  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      type: "article",
      images: [{ url: ogImage, width: 1200, height: 630 }],
    },
    twitter: {
      card: "summary_large_image",
      images: [ogImage],
    },
  };
}

Dynamic OG image edge route

For auto-generated OG images from titles, add an edge route:

// app/api/og/route.tsx
import { ImageResponse } from "next/og";

export const runtime = "edge";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const title = searchParams.get("title") || "Blog Post";
  const author = searchParams.get("author") || "";

  return new ImageResponse(
    (
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          background: "linear-gradient(135deg, #0a0a0a 0%, #1a0a2e 100%)",
          width: "100%",
          height: "100%",
          padding: "80px",
          justifyContent: "space-between",
        }}
      >
        {/* Logo/site name */}
        <div style={{ fontSize: 24, color: "#8b5cf6", fontWeight: 700 }}>
          myblog.com
        </div>
        
        {/* Post title */}
        <div
          style={{
            fontSize: title.length > 60 ? 44 : 56,
            fontWeight: 900,
            color: "#ffffff",
            lineHeight: 1.1,
          }}
        >
          {title}
        </div>
        
        {/* Author */}
        {author && (
          <div style={{ fontSize: 22, color: "#71717a" }}>
            by {author}
          </div>
        )}
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

Using the opengraph-image.tsx convention

Next.js 13+ supports a co-located opengraph-image.tsx file that auto-wires as the OG image for that route:

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

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

export default async function OgImage({
  params,
}: {
  params: { slug: string };
}) {
  const post = getPost(params.slug);
  
  return new ImageResponse(
    (
      <div style={{ /* your design */ }}>
        {post.title}
      </div>
    ),
    size
  );
}

Next.js automatically adds the og:image meta tag pointing to /blog/[slug]/opengraph-image — no manual metadata configuration needed for the image.

Preview your MDX blog OG images

After deploying, paste any blog post URL into OGFixer to verify your auto-generated OG images look correct on Twitter, LinkedIn, Discord, and Slack.

Check your OG tags free →