Skip to content

Metadata: parent title.template is applied to a page-level string title (Next treats it as absolute) #3065

Description

@owjs3901

What happened

vinext applies a parent layout's title.template to a page that exports a plain string title. Next.js 16 treats a page-level string title as absolute and does not apply the parent template, so the site name ends up duplicated under vinext.

Next.js 16.3.2   Home | Devfive
vinext           Home | Devfive | Devfive

Next.js 16.3.2   홈 | 데브파이브
vinext           홈 | 데브파이브 | 데브파이브

Only the two root routes (/en/, /ko/) differ; other routes match, because only these set a page-level string title.

Reproduction

app/[lang]/layout.tsx

export async function generateMetadata(): Promise<Metadata> {
  return {
    title: {
      default: 'Devfive',
      template: '%s | Devfive',
    },
  }
}

app/[lang]/page.tsx

export async function generateMetadata(): Promise<Metadata> {
  return { title: 'Home | Devfive' }
}
  • Next.js renders <title>Home | Devfive</title> — the string is absolute, template skipped.
  • vinext renders <title>Home | Devfive | Devfive</title> — template applied on top.

Our application deliberately writes the full title at page level precisely because Next 16 does not apply the template there (we have a code comment saying so, added while fixing an accessibility requirement for unique page titles). Under vinext that workaround double-applies.

Expected

Match Next.js: a page-level title: string is absolute; the parent title.template applies only to title.default resolution for child segments that do not set their own title, and to title: { absolute: ... } / nested title objects per the Metadata API rules.

Why it matters

<title> is user-visible in the tab and in search results, so this is an SEO/UX regression rather than a cosmetic difference. It was caught by a golden-master diff comparing SSR HTML between the two runtimes.

Environment

  • vinext 1.0.0-beta.8, Vite 8.2.2
  • Next.js 16.3.2 API surface, App Router, generateMetadata
  • next-intl for localization (the title strings come from a plain record, not from next-intl, so the plugin is not involved)
  • --platform=node, output: 'standalone'

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions