企业级官网全栈(React·Next.js·Tailwind·Axios·Headless UI·RHF·i18n)实战教程-第六章:Metadata 与 SEO(企业级优化)
本文介绍了Next.js 13+在企业级项目中的SEO与元数据优化方案。主要内容包括:1) 全局Metadata配置,支持类型安全和OpenGraph分享卡片;2) 多语言SEO实现,自动生成href-lang标签;3) 页面级SEO动态生成,适用于产品/新闻详情页;4) JSON-LD结构化数据增强搜索权重;5) robots.txt和sitemap自动生成。通过完整的SEO实践清单,帮助项目达
第六章: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.tsx或layout.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&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&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 指标优化
配套代码
更多推荐



所有评论(0)