Learn

/

Open Graph

Open Graph

8 patterns

og:title, og:image, og:description, and the rest of the Open Graph protocol. These control how your links appear when shared on LinkedIn, Facebook, Slack, Teams, and Discord. You will hit this when a shared link shows a blank card or the wrong image.

Avoid
// app/about/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About Us | Acme Corp",
  // No openGraph defined, hoping
  // the title tag is enough
};
// app/about/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About Us | Acme Corp",
  // No openGraph defined, hoping
  // the title tag is enough
};

Prefer
// app/about/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About Us | Acme Corp",
  openGraph: {
    title: "About Us",
    description: "Learn about Acme Corp and our team.",
  },
};
// app/about/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About Us | Acme Corp",
  openGraph: {
    title: "About Us",
    description: "Learn about Acme Corp and our team.",
  },
};
Why avoid

Without explicit Open Graph tags, platforms like Facebook, LinkedIn, and Slack must guess what to display. They may use the full <title> (including the brand suffix) or pull random text from the page body, resulting in an unappealing share card.

Why prefer

While some platforms fall back to the <title> tag, explicitly setting og:title gives you control over how the link appears when shared. You can use a shorter, cleaner title without the brand suffix that the title template adds.

The Open Graph protocol
Avoid
// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  openGraph: {
    images: [
      {
        url: "/og.png",
        width: 400,
        height: 400,
      },
    ],
  },
};
// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  openGraph: {
    images: [
      {
        url: "/og.png",
        width: 400,
        height: 400,
      },
    ],
  },
};

Prefer
// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  openGraph: {
    images: [
      {
        url: "/og.png",
        width: 1200,
        height: 630,
        alt: "Acme Corp - Build better products",
      },
    ],
  },
};
// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  openGraph: {
    images: [
      {
        url: "/og.png",
        width: 1200,
        height: 630,
        alt: "Acme Corp - Build better products",
      },
    ],
  },
};
Why avoid

A 400x400 square image gets cropped awkwardly on most platforms because they expect a landscape format. Facebook and LinkedIn will either stretch it, add padding, or crop the sides, making the share card look unprofessional.

Why prefer

The recommended OG image size is 1200x630 pixels (1.91:1 aspect ratio). This works well across Facebook, LinkedIn, Twitter, Slack, and Discord. Including the alt attribute improves accessibility for screen readers and provides fallback text when the image fails to load.

Facebook: Sharing best practices
Avoid
// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  openGraph: {
    title: "Acme Corp",
    description: "We build great products.",
    images: ["/og.png"],
  },
};
// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  openGraph: {
    title: "Acme Corp",
    description: "We build great products.",
    images: ["/og.png"],
  },
};

Prefer
// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  openGraph: {
    siteName: "Acme Corp",
    title: "Acme Corp",
    description: "We build great products.",
    images: ["/og.png"],
  },
};
// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  openGraph: {
    siteName: "Acme Corp",
    title: "Acme Corp",
    description: "We build great products.",
    images: ["/og.png"],
  },
};
Why avoid

Without og:site_name, platforms cannot distinguish between the page title and the website name. On Facebook, the site name appears as a subtle label that helps users identify the source. Missing it makes your share cards look less polished.

Why prefer

The siteName property adds an og:site_name meta tag that tells platforms the name of the overall website. This is shown separately from the page title in share cards. Facebook, for example, displays the site name in small text above or below the title.

Open Graph: Basic metadata
Avoid
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: "website",
    },
  };
}
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: "website",
    },
  };
}

Prefer
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: "article",
      publishedTime: post.publishedAt,
      authors: [post.author.name],
    },
  };
}
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: "article",
      publishedTime: post.publishedAt,
      authors: [post.author.name],
    },
  };
}
Why avoid

Using type: 'website' for a blog post misrepresents the content. It hides useful information like the publish date and author that platforms can display. The article type also helps search engines understand your content structure.

Why prefer

Blog posts should use type: 'article' with publishedTime and authors. This tells social platforms and search engines that the content is a dated article, not a generic webpage. Some platforms display the publish date and author in the share card.

Open Graph: Article type
Avoid
// app/products/[id]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  return {
    openGraph: {
      url: `/products/${params.id}`,
    },
    alternates: {
      canonical: `/products/${params.id}`,
    },
  };
}
// app/products/[id]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  return {
    openGraph: {
      url: `/products/${params.id}`,
    },
    alternates: {
      canonical: `/products/${params.id}`,
    },
  };
}

Prefer
// app/products/[id]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  return {
    openGraph: {
      url: `https://acme.com/products/${params.id}`,
    },
    alternates: {
      canonical: `https://acme.com/products/${params.id}`,
    },
  };
}
// app/products/[id]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  return {
    openGraph: {
      url: `https://acme.com/products/${params.id}`,
    },
    alternates: {
      canonical: `https://acme.com/products/${params.id}`,
    },
  };
}
Why avoid

Relative URLs in og:url may not resolve correctly on all platforms. Facebook's crawler, for instance, needs an absolute URL to properly aggregate share counts. Mismatched og:url and canonical values can also split social engagement metrics across different URLs.

Why prefer

Both og:url and the canonical URL should be absolute URLs pointing to the same location. Using metadataBase in the root layout can resolve relative paths, but explicit absolute URLs are clearer and ensure consistency. The og:url tells platforms which URL to associate with shares and likes.

Next.js: metadataBase
Avoid
// app/layout.tsx
export const metadata: Metadata = {
  openGraph: {
    title: "Acme Corp",
    locale: "en",
  },
};
// app/layout.tsx
export const metadata: Metadata = {
  openGraph: {
    title: "Acme Corp",
    locale: "en",
  },
};

Prefer
// app/layout.tsx
export const metadata: Metadata = {
  openGraph: {
    title: "Acme Corp",
    locale: "en_US",
    alternateLocale: ["de_DE", "fr_FR"],
  },
};
// app/layout.tsx
export const metadata: Metadata = {
  openGraph: {
    title: "Acme Corp",
    locale: "en_US",
    alternateLocale: ["de_DE", "fr_FR"],
  },
};
Why avoid

Using just en without a territory code does not follow the Open Graph specification, which expects the language_TERRITORY format. Without alternateLocale, platforms have no way to know your site offers content in other languages.

Why prefer

The og:locale tag uses the language_TERRITORY format (e.g., en_US, not just en). Including alternateLocale tells platforms that this content is available in other languages, which helps with content discovery and prevents duplicate content issues across locales.

Open Graph: Optional metadata
Avoid
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  return {
    openGraph: {
      images: ["/default-og.png"],
    },
  };
}
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  return {
    openGraph: {
      images: ["/default-og.png"],
    },
  };
}

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

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

export default async function OGImage({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPost(params.slug);
  return new ImageResponse(
    (
      <div style={{
        display: "flex",
        fontSize: 48,
        background: "#111",
        color: "#fff",
        width: "100%",
        height: "100%",
        padding: 60,
        alignItems: "center",
      }}>
        {post.title}
      </div>
    ),
    { ...size }
  );
}
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";

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

export default async function OGImage({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPost(params.slug);
  return new ImageResponse(
    (
      <div style={{
        display: "flex",
        fontSize: 48,
        background: "#111",
        color: "#fff",
        width: "100%",
        height: "100%",
        padding: 60,
        alignItems: "center",
      }}>
        {post.title}
      </div>
    ),
    { ...size }
  );
}
Why avoid

Using the same static image for every blog post means all shared links look identical. Users scrolling through social feeds cannot distinguish between posts. Dynamic OG images significantly improve click-through rates by showing relevant, unique content.

Why prefer

The opengraph-image.tsx file convention generates a unique OG image for each dynamic route. Next.js automatically sets the correct og:image meta tags. Each blog post gets its own branded image with the post title, which looks far more engaging in share cards.

Next.js: opengraph-image
Avoid
// app/products/[id]/page.tsx
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  const res = await fetch(
    `https://api.acme.com/products/${params.id}`
  );
  const product = await res.json();
  return { title: product.name };
}

export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  const res = await fetch(
    `https://api.acme.com/products/${params.id}`,
    { cache: "no-store" }
  );
  const product = await res.json();
  return <h1>{product.name}</h1>;
}
// app/products/[id]/page.tsx
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  const res = await fetch(
    `https://api.acme.com/products/${params.id}`
  );
  const product = await res.json();
  return { title: product.name };
}

export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  const res = await fetch(
    `https://api.acme.com/products/${params.id}`,
    { cache: "no-store" }
  );
  const product = await res.json();
  return <h1>{product.name}</h1>;
}

Prefer
// app/products/[id]/page.tsx
import type { Metadata } from "next";

async function getProduct(id: string) {
  const res = await fetch(
    `https://api.acme.com/products/${id}`
  );
  return res.json();
}

export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  const product = await getProduct(params.id);
  return { title: product.name };
}

export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  const product = await getProduct(params.id);
  return <h1>{product.name}</h1>;
}
// app/products/[id]/page.tsx
import type { Metadata } from "next";

async function getProduct(id: string) {
  const res = await fetch(
    `https://api.acme.com/products/${id}`
  );
  return res.json();
}

export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  const product = await getProduct(params.id);
  return { title: product.name };
}

export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  const product = await getProduct(params.id);
  return <h1>{product.name}</h1>;
}
Why avoid

Duplicating the fetch call with different cache options (no-store in one, default in the other) prevents Next.js from deduplicating the requests. This means two separate network calls for the same data, and the different caching strategies can cause the metadata and page content to show different information.

Why prefer

Next.js automatically deduplicates fetch calls with the same URL and options. By extracting the fetch into a shared function, both generateMetadata and the page component call it, but only one network request is made. This keeps the code DRY and the data consistent.

Next.js: Data fetching and caching