Medusa.js Open Graph Tags: Add OG Meta to Your Headless Store

How to add og:title, og:image, and og:description to Medusa-powered storefronts — with product metadata, Next.js Starter, and dynamic OG images.

Medusa + Next.js Starter and OG tags

Medusa is a headless commerce platform. Your storefront is a separate Next.js app that queries the Medusa backend API. OG tags are generated in the Next.js layer usinggenerateMetadata — fed with product data from@medusajs/medusa-js or the REST API.

The official Medusa Next.js Starter already includes basic metadata generation, but it lacks per-product OG images. This guide covers how to add proper OG support.

Product page OG tags

// app/[countryCode]/(main)/products/[handle]/page.tsx
import type { Metadata } from "next";
import Medusa from "@medusajs/medusa-js";

const medusa = new Medusa({
  baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,
  maxRetries: 3,
});

async function getProduct(handle: string) {
  const { products } = await medusa.products.list({
    handle,
    expand: "images,variants",
  });
  return products[0] || null;
}

export async function generateMetadata({
  params,
}: {
  params: { handle: string };
}): Promise<Metadata> {
  const product = await getProduct(params.handle);
  
  if (!product) return { title: "Product not found" };
  
  // Use first product image for OG
  const ogImage = product.thumbnail || product.images?.[0]?.url;
  
  return {
    title: product.title,
    description: product.description || `Shop ${product.title} on our store`,
    openGraph: {
      title: product.title,
      description: product.description || `Shop ${product.title}`,
      type: "website",
      images: ogImage
        ? [
            {
              url: ogImage,
              width: 1200,
              height: 630,
              alt: product.title,
            },
          ]
        : [],
    },
    twitter: {
      card: "summary_large_image",
      title: product.title,
      description: product.description || `Shop ${product.title}`,
      images: ogImage ? [ogImage] : [],
    },
  };
}

Dynamic OG images for product pages

Product images uploaded to Medusa are often square (1:1) and won't display correctly as OG images without transformation. Create a dynamic OG image route that overlays the product title on a branded background:

// app/api/og/product/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") || "Product";
  const price = searchParams.get("price") || "";
  const imageUrl = searchParams.get("image") || "";

  return new ImageResponse(
    (
      <div
        style={{
          display: "flex",
          background: "#0f0f0f",
          width: "100%",
          height: "100%",
        }}
      >
        {/* Product image on left */}
        {imageUrl && (
          <img
            src={imageUrl}
            style={{
              width: "50%",
              height: "100%",
              objectFit: "cover",
            }}
            alt={title}
          />
        )}
        
        {/* Text on right */}
        <div
          style={{
            display: "flex",
            flexDirection: "column",
            justifyContent: "center",
            padding: "60px",
            flex: 1,
          }}
        >
          <div
            style={{
              fontSize: 48,
              fontWeight: 800,
              color: "#ffffff",
              lineHeight: 1.2,
            }}
          >
            {title}
          </div>
          {price && (
            <div
              style={{
                fontSize: 36,
                color: "#8b5cf6",
                marginTop: 20,
                fontWeight: 700,
              }}
            >
              {price}
            </div>
          )}
          <div
            style={{
              fontSize: 20,
              color: "#71717a",
              marginTop: 24,
            }}
          >
            yourstore.com
          </div>
        </div>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

// Usage in generateMetadata:
// images: [{ url: `/api/og/product?title=${encodeURIComponent(product.title)}&price=$${price}&image=${encodeURIComponent(product.thumbnail)}` }]

Collection and category OG tags

// For collection pages
export async function generateMetadata({
  params,
}: {
  params: { handle: string };
}): Promise<Metadata> {
  const { collections } = await medusa.collections.list();
  const collection = collections.find((c) => c.handle === params.handle);
  
  if (!collection) return { title: "Collection not found" };
  
  return {
    title: `${collection.title} Collection`,
    description: `Browse our ${collection.title} collection`,
    openGraph: {
      title: `${collection.title} Collection`,
      description: `Shop the ${collection.title} collection`,
      type: "website",
      // Use a branded collection OG image or generate one
      images: [
        {
          url: `/api/og/collection?title=${encodeURIComponent(collection.title)}`,
          width: 1200,
          height: 630,
        },
      ],
    },
    twitter: {
      card: "summary_large_image",
    },
  };
}

Adding custom metadata fields to Medusa products

Medusa supports custom fields via the metadata object on products. Store OG overrides there:

// When creating/updating a product via Admin API:
await medusa.admin.products.update(productId, {
  metadata: {
    og_title: "Custom Social Title for This Product",
    og_description: "A compelling description for social sharing",
    og_image_url: "https://yourcdn.com/og-images/product-custom.jpg",
  },
});

// In generateMetadata, check metadata overrides:
const ogTitle = product.metadata?.og_title || product.title;
const ogDescription = product.metadata?.og_description || product.description;
const ogImage = product.metadata?.og_image_url || product.thumbnail;

Common Medusa OG mistakes

  • Square product images: Medusa product images are often 1:1 — always generate or crop to 1200×630 for OG
  • Missing twitter:card tag: Without twitter:card: "summary_large_image", Twitter shows a small thumbnail
  • CORS on product images: Ensure your Medusa media storage (S3/MinIO) serves images with public CORS headers
  • No default fallback: Products without images need a branded fallback OG image
  • Country code in URLs: Medusa Next.js starter uses [countryCode] routing — ensure your og:url uses the canonical URL without country code

Test your Medusa storefront OG tags

Paste any product or collection URL into OGFixer to preview how your social share cards look on Twitter, Slack, Discord, and LinkedIn.

Check your OG tags free →