# Next.js metadata openGraph not inherited: why my subpages lost og:site_name

If a page in the Next.js App Router sets its own `openGraph`, the openGraph object from your layout is gone for that page. Next.js merges metadata shallowly, so `og:site_name`, `og:locale` and any other field you only set in the layout disappear. The fix I use is one helper that builds the full `openGraph` and `twitter` objects for every subpage, plus a build check that fails when a tag is missing.

I found this on my own site, [mohamedgado.com](https://www.mohamedgado.com/projects/portfolio). `/now`, `/projects` and all 55 case studies were shipping without `og:site_name`, `og:locale`, `twitter:site` and `twitter:creator`. The root layout set all four. Nothing in the browser looked wrong, and nothing errored.

## Does Next.js merge openGraph metadata from the layout?

No, not field by field. The Next.js 16 docs for `generateMetadata` say that metadata objects exported from several segments of one route are shallowly merged, and that duplicate keys are replaced by the segment closest to the page. They also say it directly for nested fields: `openGraph` defined in an earlier segment is overwritten by the last segment that defines it.

The docs give the example. The layout sets `openGraph.title` and `openGraph.description`. The blog page sets only `openGraph.title`. The output has the page's `og:title` and no `og:description`, because the page's whole `openGraph` object replaced the layout's.

The same page does inherit when it does not set `openGraph` at all. A subpage with only a `title` still gets every Open Graph tag from the layout. The trouble starts the moment a page adds `openGraph` to change one field, such as the title or the image. That is the normal thing to do on a case study or a blog post, so it hits most real sites.

## What did the broken setup look like?

My root layout carried the full set:

```tsx
// app/layout.tsx
export const metadata: Metadata = {
  openGraph: {
    locale: siteConfig.openGraph.locale,
    siteName: siteConfig.name,
    url: siteConfig.links.websiteUrl,
    type: 'website',
    title: `${siteConfig.name} — ${siteConfig.tagline}`,
    description: siteConfig.description,
  },
  twitter: {
    card: 'summary_large_image',
    creator: siteConfig.twitterTag,
    site: siteConfig.twitterTag,
  },
}
```

A subpage then set what it needed, which looks harmless. This is a simplified version of what the case-study page did before the helper:

```tsx
// app/projects/[slug]/page.tsx (before)
return {
  title: project.title,
  openGraph: {
    title: `${project.title} | Mohamed Gado`,
    description,
    url: `/projects/${slug}`,
    images: [ogImage],
  },
  twitter: {
    card: 'summary_large_image',
    title: `${project.title} | Mohamed Gado`,
    description,
  },
}
```

This page's `openGraph` has no `siteName` and no `locale`, so neither tag is rendered. Its `twitter` has no `site` or `creator`, so those are gone too. The layout's values were replaced, not combined.

## How do I check the rendered tags?

Read the HTML the server sends, not the code. A dev tool that shows the metadata object tells you what you wrote, and the question is what crawlers receive. I curl the page and grep for the tag:

```bash
curl -s https://www.mohamedgado.com/projects/masareef | grep -o '<meta[^>]*og:site_name[^>]*>'
curl -s https://www.mohamedgado.com/projects/masareef | grep -oE '<meta[^>]*(og:locale|twitter:site|twitter:creator)[^>]*>'
```

An empty result means the tag is missing. I ran this against production before writing any code, because I wanted to know the gap was real on every subpage and not something I had inferred from reading the merge docs. It was.

## What is the fix for Next.js openGraph not inheriting?

The docs suggest pulling shared fields into a variable and spreading it into each page's `openGraph`. That works, but it leaves it to every page author to remember the spread. I had 55 case studies built from one template plus a few static pages, so I put the whole thing in a function in `lib/social-meta.ts`:

```ts
import type { Metadata } from 'next'
import { siteConfig } from '@/config/site'

type SocialMetaInput = {
  title: string
  description: string
  /** Root-relative path, resolved against the layout's `metadataBase`. */
  path: string
  images: NonNullable<Metadata['openGraph']>['images']
  /** Case studies are articles; set this and `og:type` becomes `article`. */
  article?: { modifiedTime?: string }
}

export function buildSocialMeta({
  title,
  description,
  path,
  images,
  article,
}: SocialMetaInput): Pick<Metadata, 'openGraph' | 'twitter'> {
  const shared = {
    siteName: siteConfig.name,
    locale: siteConfig.openGraph.locale,
    title,
    description,
    url: path,
    images,
  }
  const twitter: Metadata['twitter'] = {
    card: 'summary_large_image',
    site: siteConfig.twitterTag,
    creator: siteConfig.twitterTag,
    title,
    description,
  }
  if (!article) {
    return { openGraph: { ...shared, type: 'website' }, twitter }
  }
  return {
    openGraph: {
      ...shared,
      type: 'article',
      authors: [siteConfig.links.websiteUrl],
      ...(article.modifiedTime ? { modifiedTime: article.modifiedTime } : {}),
    },
    twitter,
  }
}
```

A page spreads the result into its metadata:

```tsx
// app/projects/[slug]/page.tsx (after)
return {
  title: project.title,
  alternates: { canonical: `/projects/${slug}` },
  ...buildSocialMeta({
    title: `${project.title} | Mohamed Gado`,
    description,
    path: `/projects/${slug}`,
    images: [ogImage],
  }),
}
```

Because `buildSocialMeta` returns both objects complete, the shallow merge no longer matters: the page's `openGraph` replaces the layout's, and the replacement already contains everything the layout had. The site name, locale and handles come from `siteConfig`, the same source the layout reads, so they cannot drift apart.

The helper also gave me a single place to add article fields. Case studies now pass `article`, which sets `og:type` to `article` and adds `article:modified_time` and `article:author`.

One detail in the helper: I leave `twitter.images` unset on purpose. My code comment says Next falls back to the `openGraph` images, with alt text and dimensions included, so the image is declared once.

## How do I stop it coming back?

A convention in a README does not survive the next new page. I added a check to the postbuild step. `scripts/check-index-parity.ts` reads every prerendered page and asserts that it carries `og:site_name`, `og:locale` and both Twitter handles. Before the fix it reported 57 failures. After, 0.

The rule written down in my lessons file is short: a page that sets `openGraph` or `twitter` goes through `buildSocialMeta`, and the build fails otherwise. Because the check runs on built HTML, it also catches pages I did not think of, such as a new route someone adds later and forgets to wire up.

## What else should I check on pages that override metadata?

Other nested fields behave the same way. The docs name `robots` next to `openGraph` as a field that is overwritten wholesale. If a layout sets `robots: { index: true, follow: true, googleBot: {...} }` and one page sets `robots: { index: false }`, the page does not keep the `googleBot` block. Anything that is an object in the Metadata type is a candidate: `alternates`, `icons`, `twitter`, `appLinks` and so on.

For each one, ask whether the layout sets it and whether any page sets it again. If both are true, the page's version has to be complete. A helper function, a spread of a shared object, or a build-time check on the output are all workable. I picked the helper and the check together, since the helper prevents the mistake and the check proves it.

The change was small, and nothing on the site looked different afterwards. What changed was the HTML that link previews and search engines read for 57 pages. If you set `openGraph` on any page below your root layout, curl one of those pages today and look for `og:site_name`.
