How to Manage Custom Meta Tags with Nextjs Metadata Static Objects

Export a typed Metadata object from any layout or page file and use the other field to inject arbitrary <meta> tags, or use generateMetadata for dynamic data, ensuring you set metadataBase for absolute URLs.

Next.js 13+ App Router revolutionizes <head> management through static metadata objects and the generateMetadata function. These APIs are processed by the framework's internal resolver to produce SEO-friendly tags without manual DOM manipulation. Understanding how Nextjs metadata flows from source files to HTML elements ensures type-safe, maintainable SEO configurations.

How Nextjs Metadata Resolution Works

The framework processes metadata through a four-stage pipeline defined in packages/next/src/lib/metadata/resolve-metadata.ts. First, it collects exports from every layout and page segment in the route tree. Then it resolves each export—whether a static object or generateMetadata function—into a ResolvedMetadata shape. Next, it merges these objects using accumulateMetadata, applying defaults from packages/next/src/lib/metadata/default-metadata.tsx, handling metadataBase normalization, and deduplicating icons. Finally, packages/next/src/lib/metadata/metadata.tsx invokes createMetadataElements to render the React components that output actual <meta> and <link> tags.

Key implementation files include:

Declaring Static Metadata Objects

The simplest approach exports a typed object from layout.tsx or page.tsx. This method is fully typed when you import Metadata from 'next'.

Using the other Field for Custom Meta Tags

For non-standard meta tags, use the other field, which accepts a record of string keys mapping to string, number, or array values. According to metadata-interface.ts, this field is typed as { [name: string]: string | number | (string | number)[] }.

// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    default: 'My Application',
    template: '%s | My Application',
  },
  description: 'A modern Next.js application',
  other: {
    author: 'Development Team',
    'theme-color': '#000000',
    keywords: ['nextjs', 'react', 'typescript'],
  },
}

The framework renders these as <meta name="author" content="Development Team"> and handles arrays by creating multiple elements for the same name. The BasicMeta component in basic.tsx (lines 119-130) iterates over this map to generate the final HTML.

Configuring metadataBase for Absolute URLs

When referencing relative paths for Open Graph images or canonical URLs, define metadataBase to ensure crawlers receive fully qualified URLs. The resolver normalizes this base via normalizeMetadataBase in resolve-metadata.ts (lines 48-56).

export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  openGraph: {
    images: [{ url: '/og-image.png' }], // Resolves to https://example.com/og-image.png
  },
}

Generating Dynamic Meta Tags

For route-dependent metadata, export an async generateMetadata function instead of a static object. Next.js calls this function during server-side rendering, passing the same params and searchParams received by the page component.

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

export async function generateMetadata({
  params,
}: {
  params: { slug: string }
}): Promise<Metadata> {
  const post = await fetchPost(params.slug)

  return {
    title: post.title,
    description: post.excerpt,
    other: {
      'article:published_time': post.publishedAt,
      'article:author': post.authorName,
    },
  }
}

During the merge phase, the framework combines this generated object with parent layout metadata, preserving title template inheritance (handled in resolve-metadata.ts lines 77-84). Do not export both metadata and generateMetadata from the same file, as the system warns against and ignores one export.

Nextjs Metadata Best Practices

Follow these implementation guidelines to avoid common pitfalls:

  • Type your exports. Always import Metadata from 'next' to enable IntelliSense and catch deprecated fields at compile time.
  • Use other for arbitrary tags. Avoid manually rendering <head> elements; the other field guarantees proper escaping and rendering order via OtherMeta in the generation pipeline.
  • Separate viewport configuration. Move themeColor, colorScheme, and viewport properties to a dedicated export const viewport object. Placing them in metadata triggers warnings (see resolve-metadata.ts lines 21-24) and will be removed in future versions.
  • Set metadataBase globally. Define this in your root layout to ensure all relative Open Graph and Twitter image URLs resolve correctly across environments.
  • Leverage title templates. Use the { default: string; template: string } object to let child routes inject their titles while maintaining brand consistency (e.g., "Post Title | Site Name").
  • Prioritize icon placement. Place favicon files in the deepest layout segment; the resolver uses leafSegmentStaticIcons to ensure the correct <link rel="icon"> appears first.
  • Minimize blocking data fetches. Only await data essential for SEO in generateMetadata to prevent "metadata fetch timeout" errors and degraded TTFB.

Full-Stack Implementation Example

This example demonstrates a root layout with static global metadata and a dynamic page extending it:

// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  metadataBase: new URL('https://demo.vercel.app'),
  title: { default: 'Demo Site', template: '%s – Demo Site' },
  description: 'Production-ready Next.js metadata implementation',
  alternates: { canonical: '/' },
  other: {
    'msapplication-TileColor': '#da532c',
    'x-custom-platform': 'web',
    keywords: ['nextjs', 'app-router', 'metadata'],
  },
}
// app/products/[id]/page.tsx
import type { Metadata } from 'next'

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

  return {
    title: product.name,
    description: product.summary,
    openGraph: {
      title: product.name,
      images: [{ url: `/og/products/${params.id}.png` }],
    },
    other: {
      'product:price:amount': product.price,
      'product:price:currency': 'USD',
    },
  }
}

The resulting HTML includes:

<title>Wireless Headphones – Demo Site</title>
<meta name="description" content="Noise-cancelling over-ear headphones">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="x-custom-platform" content="web">
<meta name="keywords" content="nextjs,app-router,metadata">
<meta property="og:title" content="Wireless Headphones">
<meta property="og:image" content="https://demo.vercel.app/og/products/123.png">
<meta name="product:price:amount" content="299.00">

Summary

  • Export a typed Metadata object from layouts or pages to define static tags, using the other field for custom meta properties.
  • Implement generateMetadata for dynamic, data-driven tags that merge automatically with parent metadata.
  • Always set metadataBase in your root layout when using relative URLs for Open Graph images or canonical links.
  • Move viewport-related settings (themeColor, viewport) to a separate viewport export to avoid deprecation warnings.
  • Reference packages/next/src/lib/metadata/resolve-metadata.ts for the merge logic and packages/next/src/lib/metadata/generate/basic.tsx for the rendering implementation.

Frequently Asked Questions

What is the difference between metadata and generateMetadata in Next.js?

Static metadata objects are evaluated at build time for pages with fixed SEO requirements, while generateMetadata is an async function called during rendering for dynamic routes. According to the source code in resolve-metadata.ts, the system treats both exports identically during the resolution phase, but you cannot use both in the same file.

How do I add multiple values for the same meta tag name?

Pass an array to the other field in your metadata object. The type definition in metadata-interface.ts allows string | number | (string | number)[], and the BasicMeta generator in basic.tsx iterates over arrays to create multiple <meta> elements with the same name attribute but different content values.

Why are my Open Graph images showing as relative URLs?

You likely omitted metadataBase. The resolver in resolve-metadata.ts (lines 48-56) uses this property to prefix relative paths. Without it, Next.js cannot generate absolute URLs required by social media crawlers, resulting in broken link previews.

Can I use both metadata and generateMetadata in the same route segment?

No. The Next.js metadata resolution system explicitly warns against and ignores one export if both are present. Choose the static object for hardcoded values or generateMetadata when you need to fetch data, as implemented in the accumulateMetadata function.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →