第六章:Metadata 与 SEO(企业级优化)

前面我们已经完成了登录、权限等核心能力,现在开始进入前端最容易忽视,但真实生产非常重要的部分:SEO 与元信息管理

Next.js 13+ 在 App Router 时代彻底重构了 Metadata 体系,和老的 next/head 不同,它是类型安全、SSR 友好、自动合并的元信息方案

你将学会:

内容 目标
metadata 静态/动态写法 为所有页面提供 SEO 信息
多语言 SEO(自动 href-lang 你的项目支持 zh/en i18n
Open Graph / Twitter Meta 分享时生成带封面预览卡片
结构化数据(JSON-LD) 提升搜索权重(企业站必须)
自定义动态标题覆盖 产品、新闻、文章详情页必备
robots / sitemap 自动生成 全站 SEO 关键配置

1.全局 Metadata 设定

📂 src/app/layout.tsx

export const metadata = {
  title: {
    default: 'Enterprise Platform',
    template: '%s | Enterprise',
  },
  description: '企业级管理平台,支持多语言、权限系统与现代前端架构。',
  keywords: ['Enterprise', 'Management', 'SaaS', 'B2B'],
  creator: 'Enterprise Inc.',
  generator: 'Next.js',
  metadataBase: new URL('https://example.com'),
  openGraph: {
    title: 'Enterprise Platform',
    description: '现代企业管理平台解决方案',
    url: 'https://example.com',
    siteName: 'Enterprise',
    images: ['/og-default.png'],
    locale: 'zh_CN',
    type: 'website',
  },
} satisfies import('next').Metadata;

satisfies Metadata = 类型安全
✔ OpenGraph 结构带分享卡片
template 允许动态替换标题


2.多语言 SEO:自动注入 <link href-lang>

App Router 自带:

export const generateStaticParams = () => locales.map(locale => ({ locale }))

我们增强 metadata 输出语言版本。

在src下创建config目录,创建metadata.ts

📁 src/config/metadata.ts

// 定义每种语言对应的元数据
export const localeMetadata = {
    zh: {
        // 1. 基础 Meta 信息(语言专属)
        title: {
            default: 'Enterprise Platform',
            template: '%s | Enterprise',
        },
        description: '企业级管理平台,支持多语言、权限系统与现代前端架构。',
        keywords: ['Enterprise', 'Management', 'SaaS', 'B2B'],
        creator: 'Enterprise Inc.',
        generator: 'Next.js',
        metadataBase: new URL('https://example.com'),

        // 2. Open Graph 元数据(社交分享用,如微信/FB 分享)
        openGraph: {
            title: 'Enterprise Platform',
            description: '现代企业管理平台解决方案',
            url: 'https://example.com',
            siteName: 'Enterprise',
            images: ['/og-default.png'],
            locale: 'zh-CN', // 声明页面语言(如 zh-CN/en-US,可根据需求细化)
            type: 'website',
        },
    },
    en: {
        title: {
            default: 'Enterprise Platform',
            template: '%s | Enterprise',
        },
        description: 'Enterprise Platform',
        keywords: ['Enterprise', 'Management', 'SaaS', 'B2B'],
        creator: 'Enterprise Inc.',
        generator: 'Next.js',
        metadataBase: new URL('https://example.com'),
        openGraph: {
            title: 'Enterprise Platform',
            description: 'Modern enterprise management platform solution',
            url: 'https://example.com',
            siteName: 'Enterprise',
            images: ['/og-default.png'],
            locale: 'en-US',
            type: 'website',
        },
    },
};

📁 src/app/[locale]/layout.tsx

generateMetadata()Next.js App Router 中是官方推荐的 SEO/Metadata 生成方式,只有放在 page/layout/route segment 级别的文件中才会自动执行,不能在普通组件中调用。

一句话总结

只要文件包含 export async function generateMetadata() 并位于 app/ 下的 page.tsxlayout.tsx 中,它就会自动执行,无需手动调用。

import '@/app/globals.css'

import { Footer } from '@/components/layout/Footer'
import { Header } from '@/components/layout/Header'
import { localeMetadata } from '@/config/metadata'
import { AuthProvider } from '@/context/AuthContext'
import { defaultLocale, Locale, locales } from '@/i18n/config'
import { Metadata } from 'next'
import { NextIntlClientProvider } from 'next-intl'
import { notFound } from 'next/navigation'
import { Providers } from './providers'

// 导出异步函数 generateMetadata(Next.js 约定的元数据生成函数)
export async function generateMetadata({
  params, // 函数接收的参数对象,包含路由参数
}: {
  // 类型标注:params 是一个 Promise,解析后包含 locale 字符串(语言标识,如 zh/en)
  params: Promise<{ locale: string }>
}): Promise<Metadata> {
  // 函数返回值是 Promise,解析后为 Next.js 的 Metadata 类型
  // 等待 params Promise 解析,获取当前页面的 locale(如 'zh'、'en')
  const { locale } = await params

  const currentMeta =
    localeMetadata[locale as keyof typeof localeMetadata] || localeMetadata[defaultLocale]

  return {
    ...currentMeta,
    // 3. 原有交替链接配置(SEO 多语言核心)
    // alternates:Next.js 专门用于配置「交替链接」的字段,核心作用是告诉搜索引擎:
    // 这个页面有其他语言版本,提升多语言站点的 SEO 效果
    alternates: {
      // canonical(规范链接):指定该页面的「权威URL」,避免搜索引擎认为不同URL是重复内容
      // 比如当前 locale 是 zh,canonical 就是 '/zh';是 en 就是 '/en'
      canonical: `/${locale}`,
      // languages:配置该页面所有可用的语言版本映射(键=语言标识,值=对应语言的URL)
      // Object.fromEntries:将数组转换为对象(比如 locales = ['zh', 'en'] 会变成 { zh: '/zh', en: '/en' })
      languages: Object.fromEntries(locales.map((lng) => [lng, `/${lng}`])),
    },
    // 4. 可选:其他语言相关配置
    authors: [{ name: '你的名字' }],
    category: 'Home',
  }
}

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<{ locale: Locale }>
}) {
  const { locale } = await params

  if (!locales.includes(locale as Locale)) {
    notFound()
  }

  const messages = (await import(`@/i18n/messages/${locale}.json`)).default

  return (
    <html lang={locale}>
      <body className="min-h-screen bg-background text-foreground">
        <Providers>
          <NextIntlClientProvider locale={locale} messages={messages}>
            <AuthProvider>
              <Header />
              {children}
              <Footer />
            </AuthProvider>
          </NextIntlClientProvider>
        </Providers>
      </body>
    </html>
  )
}

搜索引擎会知道这页面有多个语言版本(排名会提升)

关键概念解析

(1)generateMetadata 函数的核心特点
  • 异步执行:因为需要获取路由参数(params),所以函数是 async 的;
  • 路由参数(params):这里的 params 是 Promise<{ locale: string }> 类型,说明你的页面是动态路由(比如 app/[locale]/page.tsx),locale 是从路由中提取的动态参数(如 /zh 对应 locale=zh);
  • 返回值(Metadata):Next.js 内置的 Metadata 类型,包含所有可配置的页面元信息(如 title、description、alternates 等)。
(2)alternates 字段的 SEO 意义
  • canonical(规范链接):解决「重复内容」问题。比如 /zh 和 /zh/(末尾斜杠)、http:// 和 https:// 可能被搜索引擎判定为重复页面,通过 canonical 指定权威 URL,让搜索引擎只收录这个版本。
  • languages(多语言交替链接):告诉搜索引擎 “这个页面有 zh/en 等语言版本”,比如:
    • 当用户在谷歌搜索 “产品列表” 时,谷歌会根据用户的语言偏好,展示对应语言的页面;
    • 搜索引擎会将多语言版本的页面权重关联,提升整体排名。
(3)Object.fromEntries + map 的作用

假设你的 locales 数组是 ['zh', 'en', 'ja'],这段代码:

locales.map((lng) => [lng, `/${lng}`]) // 生成二维数组:[['zh', '/zh'], ['en', '/en'], ['ja', '/ja']]
Object.fromEntries(...) // 转换为对象:{ zh: '/zh', en: '/en', ja: '/ja' }

最终 languages 字段会被 Next.js 渲染为 HTML 的 <link> 标签,比如:

<link rel="alternate" hreflang="zh" href="/zh" />
<link rel="alternate" hreflang="en" href="/en" />
<link rel="alternate" hreflang="ja" href="/ja" />

4. 实际渲染效果(HTML 层面)

当用户访问 /zh 页面时,页面的 <head> 会自动生成:

  <head>
    ......
    <title>Enterprise Platform</title>
    <meta name="description" content="企业级管理平台,支持多语言、权限系统与现代前端架构。" />
    <meta name="author" content="你的名字" />
    <meta name="generator" content="Next.js" />
    <meta name="keywords" content="Enterprise,Management,SaaS,B2B" />
    <meta name="creator" content="Enterprise Inc." />
    <meta name="category" content="Home" />
    <!-- canonical 规范链接 -->
    <link rel="canonical" href="https://example.com/zh" />
    <!-- 多语言交替链接 -->
    <link rel="alternate" hrefLang="zh" href="https://example.com/zh" />
    <link rel="alternate" hrefLang="en" href="https://example.com/en" />
    <meta property="og:title" content="Enterprise Platform" />
    <meta property="og:description" content="现代企业管理平台解决方案" />
    <meta property="og:url" content="https://example.com" />
    <meta property="og:site_name" content="Enterprise" />
    <meta property="og:locale" content="zh-CN" />
    <meta property="og:image" content="https://example.com/og-default.png" />
    <meta property="og:type" content="website" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content="Enterprise Platform" />
    <meta name="twitter:description" content="现代企业管理平台解决方案" />
    <meta name="twitter:image" content="https://example.com/og-default.png" />
    ......
  </head>

当用户访问 /en 页面时,页面的 <head> 会自动生成:

  <head>
    ......
    <title>Enterprise Platform</title>
    <meta name="description" content="Enterprise Platform" />
    <meta name="author" content="你的名字" />
    <meta name="generator" content="Next.js" />
    <meta name="keywords" content="Enterprise,Management,SaaS,B2B" />
    <meta name="creator" content="Enterprise Inc." />
    <meta name="category" content="Home" />
    <!-- canonical 规范链接 -->
    <link rel="canonical" href="https://example.com/en" />
    <!-- 多语言交替链接 -->
    <link rel="alternate" hrefLang="zh" href="https://example.com/zh" />
    <link rel="alternate" hrefLang="en" href="https://example.com/en" />
    <meta property="og:title" content="Enterprise Platform" />
    <meta property="og:description" content="Modern enterprise management platform solution" />
    <meta property="og:url" content="https://example.com" />
    <meta property="og:site_name" content="Enterprise" />
    <meta property="og:locale" content="en-US" />
    <meta property="og:image" content="https://example.com/og-default.png" />
    <meta property="og:type" content="website" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content="Enterprise Platform" />
    <meta name="twitter:description" content="Modern enterprise management platform solution" />
    <meta name="twitter:image" content="https://example.com/og-default.png" />
    ......
</head>

3.页面级 SEO(产品)

产品列表

📁 src/app/[locale]/products/page.tsx

注意:需要将html的client部分单独抽成组件,因为为了获取searchParams和数据,ProductsPage被定义为async,只有组件标注为client组件,即使用'use client'时,才能使用国际化import { useTranslations } from 'next-intl'

import { getProducts } from '@/services/product.service'
import { Product } from '@/types/product'
import ProductsClient from './components/ProductsClient'

interface Props {
  searchParams: { [key: string]: string | string[] | undefined }
}

export default async function ProductsPage({ searchParams }: Props) {
  const params = await searchParams
  const page = params.page ? parseInt(params.page as string) : 1
  const size = params.size ? parseInt(params.size as string) : 10

  const res = await getProducts({ page, size })

  const products = res.data.result.data as Product[]

  return <ProductsClient products={products} />
}

修改next.config.ts,以便在组件中可以正常使用import Image from 'next/image'

/* eslint-disable */
import type { NextConfig } from "next";
const withNextIntl = require('next-intl/plugin')('./src/i18n/request.ts')

const nextConfig: NextConfig = {
  /* config options here */
  images: {
    domains: ['dummyimage.com'],
  },
};

module.exports = withNextIntl(nextConfig)

// export default nextConfig;

📁 src/app/[locale]/products/components/ProductsClient.tsx

'use client'

import { Product } from '@/types/product'
import { useTranslations } from 'next-intl'
import Image from 'next/image'
import Link from 'next/link'

interface ProductsClientProps {
  products: Product[]
}

export default function ProductsClient({ products }: ProductsClientProps) {
  const t = useTranslations()

  return (
    <div className="container mx-auto py-8">
      <h1 className="text-3xl font-bold mb-6">{t('nav.products')}</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {products.map((product: Product) => (
          <div key={product.id} className="border rounded-lg p-4 hover:shadow-lg transition-shadow">
            <Link href={`/products/${product.id}`} key={product.id}>
              <h2 className="text-xl font-semibold mb-2">{product.name}</h2>
              <p className="text-gray-600 mb-2">{product.summary}</p>
              {product.cover && (
                <Image
                  src={product.cover}
                  alt={product.name}
                  width={300}
                  height={200}
                  className="w-full h-40 object-cover rounded mb-2"
                  priority={false}
                />
              )}
              <p className="text-lg font-bold">${product.price?.toFixed(2)}</p>
            </Link>
          </div>
        ))}
      </div>
    </div>
  )
}

📁 src/services/product.service.ts

注意:只有client组件才能被mock.js进行有效拦截解析,server组件的情况下,axios无法自动添加http://localhost:3000/api,所以会出现TypeError: Invalid URL 错误,这里我们直接通过const isServer = typeof window === 'undefined';来判断是server还是client,人工指定是直接使用mock处理请求还是调用实际的api请求。

// src/services/product.service.ts
import { http } from '@/lib/http/client';
import { getMockProductDetail, getMockProducts } from '@/mock/modules/product.mock';
import { ApiPagedListResponse, ApiSingleResponse } from '@/types';
import { Product } from '@/types/product';
import { cache } from 'react';


// 检查是否是服务器环境
const isServer = typeof window === 'undefined';

export const productService = {
    getProductDetail: cache(async (id: string) => {
        if (isServer && process.env.NEXT_PUBLIC_USE_MOCK === 'true') {
            return getMockProductDetail(id);
        } else {
            return http.get<ApiSingleResponse<Product>>(`/api/products/${id}`);
        }
    }),

    async getProducts(params?: { page?: number; size?: number }) {
        if (isServer && process.env.NEXT_PUBLIC_USE_MOCK === 'true') {
            return getMockProducts(params?.page, params?.size);
        } else {
            return http.get<ApiPagedListResponse<Product[]>>(`/api/products`, { params });
        }
    }
}

export const { getProductDetail, getProducts } = productService

📁 src/types/product.ts

// src/types/product.ts

export interface Product {
    id: string
    name: string
    summary: string
    cover?: string
    description?: string
    price?: number
    category?: string
    brand?: string
    createdAt?: string
    updatedAt?: string
}

📁 src/mock/modules/product.mock.ts

// src/mock/modules/product.mock.ts
import { ApiPagedListResult, ApiResponseHeader, ApiSingleResult } from '@/types/api-response';
import { Product } from '@/types/product';
import Mock from 'mockjs';

// Generate mock products
const mockProducts: Product[] = Mock.mock({
    'list|10-20': [
        {
            'id|+1': 1,
            name: '@title(3, 6)',
            summary: '@sentence(3, 8)',
            cover: '@image(400x300, #50B347, #FFF, png, Product)',
            description: '@paragraph(2, 4)',
            'price|10-1000.2': 10.00,
            category: '@word',
            brand: '@ctitle(2, 4)',
            createdAt: '@datetime',
            updatedAt: '@datetime'
        }
    ]
}).list.map((item: { id: number, name: string, summary: string, cover: string, description: string, price: number, category: string, brand: string, createdAt: string, updatedAt: string }) => ({
    id: item.id.toString(),
    name: item.name,
    summary: item.summary,
    cover: item.cover,
    description: item.description,
    price: item.price,
    category: item.category,
    brand: item.brand,
    createdAt: item.createdAt,
    updatedAt: item.updatedAt
}));

function createSingleApiResponse<T>(
    data: T,
    code: number = 200,
    message: string = 'Success'
): {
    header: ApiResponseHeader;
    result: ApiSingleResult<T>;
} {
    return {
        header: {
            code,
            message,
            isSuccess: code === 200,
        },
        result: {
            data,
        },
    };
}

function createPagedListApiResponse<T>(
    data: T[],
    page: number,
    size: number,
    total: number,
    code: number = 200,
    message: string = 'Success'
): {
    header: ApiResponseHeader;
    result: ApiPagedListResult<T>;
} {
    const totalPage = Math.ceil(total / size);

    return {
        header: {
            code,
            message,
            isSuccess: code === 200,
        },
        result: {
            data,
            pagination: {
                page,
                size,
                total,
                totalPage,
            }
        },
    };
}

export function createAxiosResponse<T>(
    data: T,
    status: number = 200,
    statusText: string = 'OK',
    headers: Record<string, string> = {}
): {
    data: T;
    status: number;
    statusText: string;
    headers: Record<string, string>;
} {
    return { data, status, statusText, headers };
}

function getProductDetail(id: string | undefined) {
    console.log('[Mock] Getting product detail for ID:', id);
    if (!id) {
        return createSingleApiResponse(null, 404, 'Product not found');
    }

    const product = mockProducts.find(p => p.id === id);

    if (!product) {
        return createSingleApiResponse(null, 404, 'Product not found');
    }

    return createSingleApiResponse(product);
}

function getProducts(page: number = 1, size: number = 10) {
    console.log('[Mock] Getting products for page:', page, 'size:', size);
    const pageNum = page;
    const pageSize = size;

    const start = (pageNum - 1) * pageSize;
    const end = start + pageSize;
    const data = mockProducts.slice(start, end);

    return createPagedListApiResponse(
        data,
        pageNum,
        pageSize,
        mockProducts.length
    );
}

export function setupProductMock() {
    // Mock get product detail by ID
    Mock.mock(RegExp('/api/products/(.*)'), 'get', (req: { url: string }) => {
        const url = req.url;
        const id = url.match(/\/api\/products\/(.*)/)?.[1];
        return getProductDetail(id);

    });

    // Mock get all products
    Mock.mock('/api/products', 'get', (req: { url: string }) => {
        const page = req.url.match(/page=(\d+)/)?.[1] || '1';
        const size = req.url.match(/size=(\d+)/)?.[1] || '10';
        return getProducts(parseInt(page), parseInt(size));
    });

    console.info('[Mock] Product API mock enabled');
}

export function getMockProductDetail(id: string | undefined) {
    return createAxiosResponse(getProductDetail(id));
}

export function getMockProducts(page: number = 1, size: number = 10) {
    return createAxiosResponse(getProducts(page, size));
}

📁 src/mock/index.ts

添加setupProductMock

export async function setupMock() {
    if (
        process.env.NODE_ENV !== 'development' ||
        process.env.NEXT_PUBLIC_USE_MOCK !== 'true'
    ) {
        return
    }

    if (typeof window === 'undefined') return

    const { setupAuthMock } = await import('./modules/auth.mock')
    const { setupUserMock } = await import('./modules/user.mock')
    const { setupProductMock } = await import('./modules/product.mock')
    setupAuthMock()
    setupUserMock()
    setupProductMock()

    console.info('[Mock] API mock enabled')
}

产品详细

📁 src/app/[locale]/products/[id]/page.tsx

import { getProductDetail } from '@/services/product.service'
import { Product } from '@/types/product'
import { notFound } from 'next/navigation'
import ProductClient from './components/ProductClient'

export async function generateMetadata({ params }: { params: { id: string; locale: string } }) {
  const { id } = await params
  const res = await getProductDetail(id)
  const product = res.data.result.data

  if (!product) {
    return {
      title: 'Product Not Found',
      description: 'The requested product could not be found',
    }
  }

  return {
    title: product.name,
    description: product.summary,
    openGraph: {
      title: product.name,
      description: product.summary,
      images: product.cover ? [product.cover] : ['/og-default.png'],
    },
  }
}

export default async function ProductDetailPage({
  params,
}: {
  params: { id: string; locale: string }
}) {
  const { id } = await params
  const res = await getProductDetail(id)
  const product: Product | null = res.data.result.data

  if (!product) {
    notFound()
  }

  return <ProductClient product={product} />
}

📁 src/app/[locale]/products/[id]/components/ProductClient.tsx

'use client'

import { Product } from '@/types/product'
import { useTranslations } from 'next-intl'
import Image from 'next/image'

export default function ProductClient({ product }: { product: Product }) {
  const t = useTranslations()
  return (
    <>
      <div className="container mx-auto py-8">
        <div className="max-w-4xl mx-auto">
          <div className="bg-white rounded-lg shadow-md overflow-hidden">
            {product.cover && (
              <div className="h-96 relative overflow-hidden">
                <Image
                  src={product.cover}
                  alt={product.name}
                  fill
                  sizes="100vw"
                  style={{ objectFit: 'cover' }}
                  className="rounded-t-lg"
                  priority
                />
              </div>
            )}
            <div className="p-6">
              <h1 className="text-3xl font-bold mb-4">{product.name}</h1>
              <p className="text-2xl font-semibold text-blue-600 mb-4">
                ${product.price?.toFixed(2)}
              </p>
              <p className="text-gray-600 mb-6">{product.summary}</p>
              <div className="prose max-w-none">
                <p>{product.description}</p>
              </div>
              <div className="mt-6 flex items-center space-x-4">
                {product.category && (
                  <span className="bg-gray-100 text-gray-800 px-3 py-1 rounded-full text-sm">
                    {t('product.category')}: {product.category}
                  </span>
                )}
                {product.brand && (
                  <span className="bg-gray-100 text-gray-800 px-3 py-1 rounded-full text-sm">
                    {t('product.brand')}: {product.brand}
                  </span>
                )}
              </div>
            </div>
          </div>
        </div>
      </div>
    </>
  )
}

✔ SEO 自动按内容生成,不用手写
✔ 微信/Slack 分享时出现真实封面


4.JSON-LD 结构化数据(权重增强)

📁 src/app/[locale]/products/[id]/components/ProductClient.tsx

'use client'

import { Product } from '@/types/product'
import { useTranslations } from 'next-intl'
import Image from 'next/image'

export default function ProductClient({ product }: { product: Product }) {
  const t = useTranslations()
  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify({
            '@context': 'https://schema.org',
            '@type': 'Product',
            name: product.name,
            image: product.cover,
            description: product.summary,
            brand: 'Enterprise',
          }),
        }}
      />
      <div className="container mx-auto py-8">
        <div className="max-w-4xl mx-auto">
          <div className="bg-white rounded-lg shadow-md overflow-hidden">
            {product.cover && (
              <div className="h-96 relative overflow-hidden">
                <Image
                  src={product.cover}
                  alt={product.name}
                  fill
                  sizes="100vw"
                  style={{ objectFit: 'cover' }}
                  className="rounded-t-lg"
                  priority
                />
              </div>
            )}
            <div className="p-6">
              <h1 className="text-3xl font-bold mb-4">{product.name}</h1>
              <p className="text-2xl font-semibold text-blue-600 mb-4">
                ${product.price?.toFixed(2)}
              </p>
              <p className="text-gray-600 mb-6">{product.summary}</p>
              <div className="prose max-w-none">
                <p>{product.description}</p>
              </div>
              <div className="mt-6 flex items-center space-x-4">
                {product.category && (
                  <span className="bg-gray-100 text-gray-800 px-3 py-1 rounded-full text-sm">
                    {t('product.category')}: {product.category}
                  </span>
                )}
                {product.brand && (
                  <span className="bg-gray-100 text-gray-800 px-3 py-1 rounded-full text-sm">
                    {t('product.brand')}: {product.brand}
                  </span>
                )}
              </div>
            </div>
          </div>
        </div>
      </div>
    </>
  )
}

在搜索引擎搜索中会出现更详细的展示

实际渲染效果(HTML 层面)

    <title>Jieqidqv Jdk Lrgy Swgexs Npedc | Enterprise</title>
    <meta name="description" content="Van hysqe ykxv odoqfyuj." />
    <meta name="author" content="你的名字" />
    <meta name="generator" content="Next.js" />
    <meta name="keywords" content="Enterprise,Management,SaaS,B2B" />
    <meta name="creator" content="Enterprise Inc." />
    <meta name="category" content="Home" />
    <link rel="canonical" href="https://example.com/zh" />
    <link rel="alternate" hrefLang="zh" href="https://example.com/zh" />
    <link rel="alternate" hrefLang="en" href="https://example.com/en" />
    <meta property="og:title" content="Jieqidqv Jdk Lrgy Swgexs Npedc" />
    <meta property="og:description" content="Van hysqe ykxv odoqfyuj." />
    <meta property="og:image" content="http://dummyimage.com/400x300/50B347/FFF.png&amp;text=Product" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content="Jieqidqv Jdk Lrgy Swgexs Npedc" />
    <meta name="twitter:description" content="Van hysqe ykxv odoqfyuj." />
    <meta name="twitter:image" content="http://dummyimage.com/400x300/50B347/FFF.png&amp;text=Product" />
......
    <script type="application/ld+json">{
        "@context": "https://schema.org",
        "@type": "Product",
        "name": "Jieqidqv Jdk Lrgy Swgexs Npedc",
        "image": "http://dummyimage.com/400x300/50B347/FFF.png&text=Product",
        "description": "Van hysqe ykxv odoqfyuj.",
        "brand": "Enterprise"
      }</script>

5.设置 robots 与 sitemap(SEO 关键)

📁 src/app/robots.ts

import type { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: '*', allow: '/' },
    sitemap: 'https://example.com/sitemap.xml',
  }
}

📁 src/app/sitemap.ts

import type { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    { url: 'https://example.com', lastModified: new Date() },
    { url: 'https://example.com/zh', lastModified: new Date() },
    { url: 'https://example.com/en', lastModified: new Date() },
  ]
}

6.你需要掌握的实践 Checklist

是否完成
Global Metadata(logo、品牌、默认标题)
多语言 alternate
动态页面 SEO generateMetadata
OpenGraph 分享卡片
JSON-LD 结构化数据
robots + sitemap

若全开启,你的网站就已经达成 可收录、可分享、可语言切换、可呈现封面信息
真正达到企业 SEO 标准。


Next Step:第七章预告

性能优化 & 图片策略 + Loading/Suspense + Prefetch 预加载

  • next/image 全局封装

  • 不使用 Suspense 的加载策略会导致“白屏”

  • 代码切割 & RSC 缓存

  • ISR / SSG / SSR 选择策略

  • Lighthouse 指标优化


 配套代码

https://gitcode.com/IT_ORACLE/enterprise-web

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐