Meta Tags

How to set static and dynamic metadata in Nebutra-Sailor — title templates, Open Graph, Twitter cards, canonical URLs, robots directives, and per-page vs layout-level metadata.

How metadata works in Next.js 16

In the Next.js App Router, metadata is exported from page.tsx or layout.tsx files. Next.js merges metadata from the nearest layout.tsx with metadata from the current page.tsx, with page-level metadata taking priority on any shared field.

There are two export forms:

FormWhen to use
export const metadataStatic titles and descriptions that do not depend on route params or external data
export async function generateMetadata()Dynamic titles that depend on route params, database queries, or CMS content

Title template

Define a title template in your root or segment layout so that every page gets a consistent suffix:

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: {
    template: "%s | Nebutra",
    default: "Nebutra — AI-native SaaS Platform",
  },
  description:
    "Build and ship AI-powered SaaS products faster with Nebutra.",
};

Child pages export only title: "Pricing" and the rendered <title> becomes Pricing | Nebutra.

Static metadata example

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Pricing",
  description:
    "Simple, transparent pricing. Start free, upgrade when you grow.",
  openGraph: {
    title: "Pricing — Nebutra",
    description: "Simple, transparent pricing.",
    url: "https://nebutra.com/en/pricing",
    siteName: "Nebutra",
    images: [
      {
        url: "https://nebutra.com/og/pricing.png",
        width: 1200,
        height: 630,
        alt: "Nebutra Pricing",
      },
    ],
    locale: "en_US",
    type: "website",
  },
  twitter: {
    card: "summary_large_image",
    title: "Pricing — Nebutra",
    description: "Simple, transparent pricing.",
    images: ["https://nebutra.com/og/pricing.png"],
  },
};

Dynamic metadata with generateMetadata

Use generateMetadata for pages that fetch content at request time, such as blog posts:

import type { Metadata } from "next";
import { getPost } from "@/lib/sanity";
import { notFound } from "next/navigation";

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

  if (!post) return {};

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: "article",
      publishedTime: post.publishedAt,
      authors: [post.author.name],
      images: [
        {
          url: post.ogImage,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    twitter: {
      card: "summary_large_image",
      title: post.title,
      description: post.excerpt,
      images: [post.ogImage],
    },
    alternates: {
      canonical: `https://nebutra.com/${lang}/blog/${slug}`,
    },
  };
}

params is a Promise in Next.js 16. Always await it before reading values. Skipping the await returns undefined and causes a runtime error.

Canonical URLs

Set alternates.canonical on every page that has a definitive URL. This prevents duplicate-content penalties when the same content is accessible at multiple paths (e.g., with and without locale prefix).

export const metadata: Metadata = {
  alternates: {
    canonical: "https://nebutra.com/en/pricing",
    languages: {
      en: "https://nebutra.com/en/pricing",
      zh: "https://nebutra.com/zh/pricing",
      ja: "https://nebutra.com/ja/pricing",
    },
  },
};

The languages map generates <link rel="alternate" hreflang="..."> tags for each locale, which search engines use to serve the correct language version.

Robots directives

Control crawler access per page using the robots metadata field:

// Default — index and follow
export const metadata: Metadata = {
  robots: { index: true, follow: true },
};

// Prevent indexing (e.g., admin pages, draft previews)
export const metadata: Metadata = {
  robots: { index: false, follow: false, noarchive: true },
};

The root robots.ts file sets site-wide defaults. Page-level robots metadata overrides the defaults for that page only.

Metadata fields reference

FieldTypePurpose
titlestring | TemplateStringPage title. Use template in layout, plain string in pages.
descriptionstringMeta description. Aim for 120–160 characters.
openGraph.titlestringOG title — can differ from <title>.
openGraph.descriptionstringOG description.
openGraph.imagesOGImage[]Array of image objects with url, width, height, alt.
openGraph.typestringwebsite for pages, article for blog posts.
openGraph.publishedTimestringISO 8601 date. Only for type: "article".
twitter.cardstringsummary_large_image for posts with a hero image.
twitter.imagesstring[]Twitter card image URLs.
alternates.canonicalstringAbsolute canonical URL.
alternates.languagesRecord<string, string>hreflang map for i18n pages.
robotsRobotsCrawler directives: index, follow, noarchive, etc.

Per-page vs layout-level metadata

ConcernWhere to set it
Default title templateRoot layout.tsx
Site-wide description fallbackRoot layout.tsx
Site-wide OG siteNameRoot layout.tsx
Page-specific title and descriptionpage.tsx
Dynamic OG imagespage.tsx via generateMetadata
Canonical URLpage.tsx
hreflangpage.tsx

Never set openGraph.images in a layout — layout-level OG images are inherited by every page in that segment, which produces incorrect social previews for pages that have their own OG images.

How is this guide?

Edit on GitHub

Last updated on

On this page