Next.js Open Graph: why your pages share with no image

Next.js Open Graph metadata on a page replaces the layout's instead of merging. How one small helper fixed twenty pages that shared as bare links with no image.

By

If links to your Next.js site show up in Slack, WhatsApp or LinkedIn as plain text with no picture, the cause is often one line of metadata. In Next.js Open Graph settings, a page that defines its own openGraph object replaces the layout's openGraph entirely. Your default share image, site name and URL go with it. This post explains why that happens, how I found twenty affected pages on a stock photo site, and the small helper that fixed all of them at once.

Why a page's openGraph wipes out the layout's

The site is Fotobank, a stock photo marketplace built on Next.js. The root layout set a sensible default: a branded share card, the site name and a locale. Every page then exported its own metadata with a title and description, and many also set openGraph.title and openGraph.description so the share preview would say something specific.

That second step is where it broke. Next.js merges metadata from the layout and the page, but the merge is shallow. The generateMetadata reference says segments are "shallowly merged" and duplicate keys are replaced based on their ordering. openGraph is one key. When a page sets it, the page's object wins as a whole, and anything it leaves out is gone. A page that set a title and description under openGraph but no images ended up with no og:image tag at all.

It is an easy mistake because the layout looks like a default. You expect a nested object to fill in the gaps the way CSS cascades. It doesn't. The same rule applies to twitter, alternates and every other nested metadata field.

How to find pages that share with no image

On Fotobank this affected twenty pages: photo pages, categories, the blog, news, events, about, the license page and more. Nothing looked wrong in the browser. The page rendered fine, the title in the tab was right, and the only symptom was a bare link wherever someone shared it.

The quickest check is the rendered HTML. For each route type, open one real URL, view source and search for og:image. If it is missing, or present on the homepage and missing on a photo page, you have this bug. I check one URL per route, since every page in a route shares the same metadata code.

A few things are worth checking while you are in there:

  • og:url should be the page's own URL. If it is missing, some platforms fall back to whatever URL they were given, which is fine, but a canonical value is better.
  • og:site_name usually comes from the layout, so it disappears in the same way.
  • Image URLs must be absolute in the final HTML. Next.js resolves relative image paths against metadataBase in the root layout. The docs say a relative URL with no metadataBase configured causes a build error, so set it once and keep image paths relative.

One helper for every page's Next.js Open Graph tags

I didn't want twenty pages each repeating the site name, URL and fallback image. That is how the bug started: every page had to remember the defaults, and most didn't. So every page now goes through one helper, pageOpenGraph, that adds the page URL and site name and falls back to the default share card when the page has no image of its own.

import type { Metadata } from "next";

type OpenGraph = NonNullable<Metadata["openGraph"]>;

const SITE_NAME = "Fotobank";
const SHARE_IMAGE = "/og-default.jpg";

export function pageOpenGraph(path: string, og: OpenGraph): OpenGraph {
  const images = og.images ? [og.images].flat() : [];
  return {
    siteName: SITE_NAME,
    url: path,
    ...og,
    images: images.length ? images : [SHARE_IMAGE],
  };
}

A page then passes only what is specific to it:

export async function generateMetadata({ params }) {
  const { slug } = await params;
  const photo = await getPhoto(slug);
  return {
    title: photo.title,
    openGraph: pageOpenGraph(`/image/${slug}`, {
      title: photo.title,
      images: photo.mediumUrl,
    }),
  };
}

The spread order matters. Defaults go first so a page can override them, and images goes last so an empty or missing value can never knock out the fallback. The helper is small on purpose. It doesn't build titles or descriptions, because those differ too much between page types to be worth abstracting.

Fallbacks for Twitter/X and posts with no cover

Twitter/X reads its own twitter:* tags, and the twitter metadata key has the same shallow merge behaviour. On Fotobank, blog and news posts without a cover image also fall back to the default card for Twitter/X, so a post with no cover still shares with a picture on every platform.

My rule for any fallback: decide it once, in one place, and make the page opt out of it rather than opt in. Pages are written by different people at different times. A default that each page must remember to include will be forgotten on the next page someone adds.

The same pattern on this site

The site you are reading uses the same idea. One pageMeta helper builds the title, description, canonical URL, Open Graph and Twitter tags for every page, from a title, a description and a path. The homepage, the projects page, the blog index and every post call it. Nothing sets openGraph by hand, so nothing can drop the share image by accident.

A helper like this also makes audits cheap. When I want to change the default share card or add a field, there is one function to edit and one place to review. I took the same approach to feeds on Fotobank, where the Pinterest RSS feed for auto-publishing builds every item from one function.

Small bugs you find on the way

Going through every page's metadata turned up a side bug: a topic page built its title as the topic name plus "News", so a topic called "Industry News" was titled "Industry News News". Bugs like that survive because people rarely read the titles of pages they didn't just change.

That is the other benefit of moving metadata into one helper. You end up reading every page's metadata once, side by side, and odd output stands out. Fotobank had other page-level bugs of the same quiet kind, written up in fixing React hydration errors from dates and modals and measuring a fixed header's height into a CSS variable.

Do you need a unique og:image for every page in Next.js?

No. A page with a strong image of its own (a photo, a product, a post cover) should use it, because it makes the share preview specific. Everything else can use one well-designed default card. The only real failure is no image at all, and a fallback in a shared helper prevents that.

Where I'd start

  • View source on one URL per route type and search for og:image.
  • Set metadataBase in the root layout so relative image paths resolve.
  • Route every page's openGraph through one helper that fills in the site name, URL and a fallback image.
  • Do the same for twitter, since it is merged the same shallow way.

Want this kind of work on your product?

Get in touch