Menu

Metadata 元数据系统

什么是 Metadata?

Metadata(元数据)是描述网页内容的信息,包含在 HTML <head> 标签中,对 SEO 和社交媒体分享至关重要。

示例:

<head>
  <title>Nexty.dev - Build and ship your SaaS faster</title>
  <meta name="description" content="Next.js SaaS starter template..." />
  <meta property="og:title" content="Nexty.dev" />
  <meta property="og:image" content="https://nexty.dev/og.png" />
  <link rel="canonical" href="https://nexty.dev/" />
  <link rel="alternate" hreflang="en-US" href="https://nexty.dev/" />
  <link rel="alternate" hreflang="zh-CN" href="https://nexty.dev/zh" />
</head>

Nexty.dev 的 Metadata 系统

核心函数:constructMetadata()

位置:lib/metadata.ts

作用:

  • 统一生成所有页面的 Metadata
  • 自动处理多语言
  • 自动生成 canonical 和 hreflang
  • 自动生成 Open Graph 和 Twitter 卡片

函数解释

lib/metadata.ts
type MetadataProps = {
  page?: string              // 页面名称(已废弃,无需配置)
  title?: string             // 页面标题
  description?: string       // 页面描述
  images?: string[]          // OG 图片(相对路径或绝对路径)
  noIndex?: boolean          // 是否禁止索引(默认 false)
  locale?: Locale            // 当前语言(en, zh, ja)
  path?: string              // 页面路径(必须以 / 开头)
  canonicalUrl?: string      // 规范 URL(可选)
  availableLocales?: string[] // 可用语言列表(可选)
  useDefaultOgImage?: boolean // 默认 true
}

1. title - 页面标题

title: "About Us"

生成规则:

  • 首页: {title} - {siteConfig.tagLine}
    • 示例:Nexty.dev - Build and ship your SaaS faster
  • 其他页: {title} | {siteConfig.name}
    • 示例:About Us | Nexty.dev

如果不传,将使用多语言文件中的默认值(Home 命名空间的 title

2. description - 页面描述

description: "Learn more about our mission and team."

作用:

  • 显示在搜索结果中
  • 显示在社交媒体卡片中

如果不传,将使用多语言文件中的默认值。

3. images - OG 图片

images: post.featuredImageUrl ? [post.featuredImageUrl] : [],

支持格式:

  • 相对路径:["images/about.png"]https://nexty.dev/images/about.png
  • 绝对路径:["https://cdn.example.com/about.png"]

大多数情况下无需为页面单独配置 images,系统会自动根据页面语言选择默认图片,例如:

  • 英文:/og.png
  • 中文:/og_zh.png
  • 日文:/og_ja.png

4. noIndex - 禁止索引

noIndex: true

作用:

  • 生成 <meta name="robots" content="noindex, nofollow" />
  • 告诉搜索引擎不要索引该页面

使用场景:

  • 付费内容页
  • 登录后才能访问的页面
  • 测试页面
  • 404 页面

5. locale - 当前语言

传入当前语言标识,用于自动处理 metadata 中的语言信息和必要的多语言 URL。

locale: "zh" // en, zh, ja

作用:

  • 影响 URL 生成
  • 影响 OG 图片选择
  • 影响 <html lang="zh"> 属性

6. path - 页面路径

path 必须以 / 开头,且不要包含语言前缀,例如:

path: "/about"

construcMetadata() 会结合 localepath 自动处理多语言信息和 URL。

作用:

  • 用于生成 canonical URL,帮助搜索引擎识别页面的规范版本,避免重复内容问题
  • 用于生成 hreflang 链接,让搜索引擎了解页面的语言和地区版本,为不同地区的用户提供正确的语言版本

7. canonicalUrl - 规范 URL

canonicalUrl: "/blogs"

使用场景:

当多个 URL 指向相同内容时,例如 /blogs/blogs?ref=docs,需要告诉搜索引擎它们的规范版本 URL 都是 /blogs,这样可以避免搜索引擎将带参数的 URL 当作新页面,导致 SEO 权重分散。

通常无需手动配置此项,系统会自动引用 paht 进行配置。只有当你需要将当前页面指向不同 URL 时才需要手动配置。

8. availableLocales - 可用语言列表

availableLocales: ["en", "zh"]  // 只有英文和中文版本

使用场景:

当某个页面并非所有语言都有对应版本时(比如博客文章)。

作用:

  • 仅为存在的语言版本生成 hreflang 链接
  • 避免生成指向 404 页面的链接

示例:博客文章

// 检测哪些语言有该文章
const availableLocales: string[] = []
for (const checkLocale of LOCALES) {
  const post = await getPostBySlug(slug, checkLocale)
  if (post) availableLocales.push(checkLocale)
}
 
// 只为存在的语言生成 hreflang
constructMetadata({
  // ...
  availableLocales: availableLocales.length > 0 ? availableLocales : undefined,
})

如果不传入该参数,将为所有语言生成 hreflang 链接。

9. useDefaultOgImage - 是否使用默认 OG Image

useDefaultOgImage 默认值是 true,即使用 construcMetadata 自动匹配的 OG Image。

如果你想在动态页面,根据内容自动生成 OG Image 样式和内容,需要将 useDefaultOgImage 设置为 false,与此同时,同时在动态页面的同级目录创建 opengraph-image.tsx 文件,开发自适应内容的 OG Image 样式。

生成的 Metadata 内容

constructMetadata() 会生成以下内容:

1. 基础 Meta 标签

<title>About Us | Nexty.dev</title>
<meta name="description" content="Learn more about our mission..." />
<meta name="keywords" content="" />
<meta name="author" content="nexty.dev" />

2. Canonical URL

<link rel="canonical" href="https://nexty.dev/about" />

3. 多语言 hreflang

<link rel="alternate" hreflang="en-US" href="https://nexty.dev/about" />
<link rel="alternate" hreflang="zh-CN" href="https://nexty.dev/zh/about" />
<link rel="alternate" hreflang="ja-JP" href="https://nexty.dev/ja/about" />
<link rel="alternate" hreflang="x-default" href="https://nexty.dev/about" />

x-default 的作用:

  • 告诉搜索引擎,当用户语言不匹配时,显示哪个版本
  • 通常指向默认语言(英文)

4. Open Graph 标签

<meta property="og:type" content="website" />
<meta property="og:title" content="About Us | Nexty.dev" />
<meta property="og:description" content="Learn more about..." />
<meta property="og:url" content="https://nexty.dev/about" />
<meta property="og:site_name" content="Nexty.dev" />
<meta property="og:locale" content="en" />
<meta property="og:image" content="https://nexty.dev/og.png" />
<meta property="og:image:alt" content="About Us | Nexty.dev" />

5. Twitter Card 标签

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="About Us | Nexty.dev" />
<meta name="twitter:description" content="Learn more about..." />
<meta name="twitter:site" content="https://nexty.dev/about" />
<meta name="twitter:image" content="https://nexty.dev/og.png" />
<meta name="twitter:creator" content="@judewei_dev" />

6. Robots 标签

<!-- noIndex = false(默认) -->
<meta name="robots" content="index, follow" />
 
<!-- noIndex = true -->
<meta name="robots" content="noindex, nofollow" />

constructMetadata() 用法示例

场景 1:首页

app/[locale]/layout.tsx
export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  const t = await getTranslations({ locale, namespace: 'Home' })
  
  return constructMetadata({
    title: t('title'), // 从多语言文件读取
    description: t('description'), // 从多语言文件读取
    locale: locale as Locale, // 传入当前语言标识,用于自动处理 metadata 中的语言信息和必要的多语言 URL
    path: '/', // 首页路径
  })
}

场景 2:普通页面

app/[locale]/(basic-layout)/about/page.tsx
export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  const t = await getTranslations({ locale, namespace: 'About' })
  
  return constructMetadata({
    title: t('title'),
    description: t('description'),
    locale: locale as Locale,
    path: `/about`,
  })
}

场景 3:使用自定义 OG 图片

export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  const t = await getTranslations({ locale, namespace: 'Pricing' })
  
  return constructMetadata({
    title: t('title'),
    description: t('description'),
    locale: locale as Locale,
    path: `/pricing`,
    images: ['images/og/pricing.png'],  // 自定义 OG 图片
  })
}

场景 4:动态 OG 图片

export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { slug } = await params;
  const result = await getProductBySlug(slug);
 
  if (!result.success || !result.data) {
    return constructMetadata({
      title: "404",
      description: "Product not found",
      noIndex: true,
      path: `/product/${slug}`,
    });
  }
 
  const product = result.data;
  const fullPath = `/product/${slug}`;
 
  return constructMetadata({
    title: product.name,
    description: product.tagline,
    path: fullPath,
    useDefaultOgImage: false, // 🔑 关键:禁用默认图片
    // Next.js 会自动查找同级目录下的 opengraph-image.tsx 文件并应用。
  })
}

场景 5:博客文章(多语言)

app/[locale]/(basic-layout)/blogs/[slug]/page.tsx
export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale, slug } = await params
  const { post } = await getPostBySlug(slug, locale)
  
  if (!post) {
    return constructMetadata({
      title: "404",
      description: "Page not found",
      noIndex: true,  // 404 页面不索引
      locale: locale as Locale,
      path: `/blogs/${slug}`,
    })
  }
  
  // 检测哪些语言有该文章
  const availableLocales: string[] = []
  for (const checkLocale of LOCALES) {
    const { post: localePost } = await getPostBySlug(slug, checkLocale)
    if (localePost) {
      availableLocales.push(checkLocale)
    }
  }
  
  return constructMetadata({
    title: post.title,
    description: post.description,
    images: post.featuredImageUrl ? [post.featuredImageUrl] : [],
    locale: locale as Locale,
    path: `/blogs/${post.slug.replace(/^\//, '')}`,
    availableLocales: availableLocales.length > 0 ? availableLocales : undefined,
  })
}

场景 6:付费/受限内容

export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  
  return constructMetadata({
    title: "Premium Dashboard",
    description: "Access your premium features.",
    locale: locale as Locale,
    path: `/dashboard/premium`,
    noIndex: true,  // 付费内容不让搜索引擎索引
  })
}

场景 7:合并多个 URL 的 Canonical

// 场景1:/free-trial 和 /pricing 指向同一内容,但你想让搜索引擎认为 /pricing 是规范 URL
// 场景2:/blogs 和 /blogs?ref=docs 指向同一页面,你需要告诉搜索引擎 /blogs 是规范 URL
 
export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  
  return constructMetadata({
    title: "Start Your Free Trial",
    description: "Try all features for free.",
    locale: locale as Locale,
    path: "/free-trial",
    canonicalUrl: "/pricing",  // 指向 /pricing 作为规范 URL
  })
}

场景 8:使用外部 CDN 的 OG 图片

export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  
  return constructMetadata({
    title: "Case Study",
    description: "How we helped Company X achieve results.",
    locale: locale as Locale,
    path: "/case-studies/company-x",
    images: [
      "https://cdn.yourdomain.com/case-studies/company-x-og.png"  // 绝对 URL
    ],
  })
}