Next.js SEO 完全指南:Metadata API + 结构化数据实战
凌晨三点,我盯着 Google Search Console 的统计面板,页面上那个大大的”0”刺眼得让人想砸电脑。
这已经是产品上线第 23 天了。两个月的开发时间、无数个通宵、精心打磨的 UI、流畅的用户体验——全都在这个冰冷的”0”面前显得毫无意义。更让人抓狂的是,当我把链接分享到 Twitter 时,预览区只有一片空白。连张像样的封面图都没有。
“Next.js 不是自带 SSR 吗?怎么 SEO 还是这么差?“我翻遍了官方文档,才意识到一个残酷的事实:SSR 不等于 SEO 友好。如果你的 meta 标签配错了、结构化数据没配、Open Graph 协议不懂——搜索引擎照样把你当空气。
这种痛我相信你也感受过:辛辛苦苦做出来的产品,搜不到、分享出去也不专业,只能靠烧钱做推广。但其实只要掌握 Next.js 15 的 Metadata API 和几个关键配置,这些问题都能解决。
这篇文章会手把手教你:怎么用 Metadata API 让每个页面都有独特的 meta 标签,怎么配置结构化数据让搜索结果更吸睛,怎么实现完美的社交分享预览。更重要的是,我会告诉你 5 个最常见的 SEO 坑,帮你避免我踩过的雷。
为什么你的 Next.js 网站 SEO 很差?
SSR 不等于 SEO 友好
说实话,我一开始也是这么想的。用了 Next.js,服务端渲染,HTML 直接吐给爬虫,SEO 不就完美了吗?
天真。
后来我看了一个朋友的项目源码,打开浏览器调试工具看 HTML,发现所有页面的 <title> 都是”My App”,<meta name="description"> 要么没有,要么全是一样的内容。这就像你开了家精品店,但门口的招牌永远写着”店铺”,顾客根本不知道你卖什么。
Next.js 确实提供了 SSR 能力,但 meta 标签的配置完全是你自己的责任。如果不配,就是一堆空壳 HTML,搜索引擎抓到了也不知道你的页面讲什么。
5 个致命的 SEO 错误
我见过太多开发者踩这些坑,包括我自己:
1. 所有页面共用一个 title 和 description
最常见的错误。要么在 _document.tsx 里写死一个 title,要么压根不写。结果就是 Google 搜到你的首页、关于页、产品页,显示的标题和描述都一模一样。
想象一下,你在书店看书,所有书的封面都印着同一个书名。你会买吗?
2. 忘记配置 canonical URL,导致重复内容
这个坑特别隐蔽。你的网站可能有分页(?page=2)、筛选(?category=tech)、排序(?sort=date)等 URL 参数,这些其实是同一个页面的不同视图,但搜索引擎会认为它们是不同页面,然后惩罚你”重复内容”。
我有个客户的博客就因为这个,流量掉了 40%。加了 canonical URL 之后,两周内恢复正常。
3. 没有结构化数据,错失富媒体展示
你有没有注意过,搜索”苹果派食谱”时,有些结果会直接显示评分、烹饪时间、卡路里?那些不是 Google 猜的,是网站主动告诉 Google 的——通过结构化数据(JSON-LD)。
根据研究,实现结构化数据的网站,点击率平均提升 20-30%。这是什么概念?相当于你的流量直接涨三成,而你只需要加几行代码。
4. 图片没有 alt 属性,浪费图片搜索流量
很多开发者觉得 alt 属性是给盲人用的,跟 SEO 无关。错了。
Google 图片搜索是一个巨大的流量入口。如果你的图片有清晰的 alt 描述,就有机会在图片搜索中排名。我见过一个设计素材网站,30% 的流量来自 Google 图片搜索,就因为他们认真写了每张图的 alt。
5. 没有 sitemap.xml 和 robots.txt
这俩文件是告诉搜索引擎”我的网站有哪些页面可以抓”和”哪些不能抓”。没有 sitemap,Google 可能要好几个月才发现你的新文章。有了 sitemap,提交到 Search Console 后,新页面几天内就能被索引。
而且 Next.js App Router 现在支持自动生成 sitemap.ts 和 robots.ts,连手写都不用,为什么不配呢?
Metadata API 完全掌握(Next.js 15)
Next.js 15 的 Metadata API 是这个框架给 SEO 的最大礼物。以前你要在每个页面手写 <Head>,现在只需要导出一个对象或函数,Next.js 自动帮你处理。
静态 metadata:适合固定内容的页面
最简单的场景:你的”关于我们”页面、隐私政策页面,这些内容基本不变。直接在 page.tsx 里导出一个 metadata 对象:
// app/about/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '关于我们 - TechBlog',
description: '我们是一群热爱技术的开发者,分享前端、后端、DevOps 实战经验。',
keywords: ['技术博客', '前端开发', 'Next.js', 'React'],
authors: [{ name: '张三' }],
openGraph: {
title: '关于我们 - TechBlog',
description: '技术博客,分享开发实战经验',
url: 'https://yourdomain.com/about',
siteName: 'TechBlog',
images: [
{
url: 'https://yourdomain.com/og-about.jpg',
width: 1200,
height: 630,
}
],
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: '关于我们 - TechBlog',
description: '技术博客,分享开发实战经验',
images: ['https://yourdomain.com/og-about.jpg'],
},
}
export default function AboutPage() {
return <div>关于我们的内容...</div>
}
看到没?类型安全、IDE 自动补全、不用担心拼写错误。而且 Next.js 会自动帮你去重——如果多个地方定义了同样的 meta 标签,它会智能合并。
最佳实践:
title控制在 60 个字符以内,超出的会在搜索结果中被截断description控制在 150-160 字符,这是 Google 搜索结果摘要的理想长度openGraph.images尺寸建议 1200x630 像素,这个尺寸在 Twitter、Facebook、LinkedIn 都通用
动态 metadata:博客、产品页的救星
真正强大的是 generateMetadata 函数。比如你的博客文章页面,每篇文章的标题、描述都不一样,总不能手写吧?
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { getPostBySlug } from '@/lib/posts'
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
// 从数据库或 CMS 获取文章数据
const post = await getPostBySlug(params.slug)
return {
title: `${post.title} - TechBlog`,
description: post.excerpt,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
return <article>{post.content}</article>
}
这样每篇文章都有独特的 SEO 信息了。Google 抓取时会看到完整的 HTML,不需要客户端 JavaScript。
关键技巧:metadataBase
你有没有发现上面的代码里,图片 URL 都是绝对路径?如果你的图片路径是相对的(比如 /images/cover.jpg),需要在 root layout 配置 metadataBase:
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL('https://yourdomain.com'),
}
配了这个之后,所有相对路径会自动拼接成完整 URL。不然 Open Graph 抓取会失败,社交分享就没图了。
模板系统:统一所有页面的标题格式
你有没有注意到,很多网站的页面标题都是”页面名 | 网站名”的格式?比如”首页 | TechBlog”、“关于我们 | TechBlog”。
如果每个页面都手写后缀,太麻烦了。用 title.template 就能搞定:
// app/layout.tsx (root layout)
export const metadata: Metadata = {
title: {
template: '%s | TechBlog',
default: 'TechBlog - 技术博客',
},
description: '技术博客,分享前端、后端、DevOps 实战经验',
metadataBase: new URL('https://yourdomain.com'),
}
然后子页面只需要写页面名:
// app/about/page.tsx
export const metadata: Metadata = {
title: '关于我们', // 最终渲染成 "关于我们 | TechBlog"
}
首页不想要后缀?用 title.absolute:
// app/page.tsx
export const metadata: Metadata = {
title: {
absolute: 'TechBlog - 技术博客首页', // 不会套用 template
},
}
这套机制帮你保持标题的一致性,而且改起来特别方便——只需要在 root layout 改一次模板,所有页面自动生效。
结构化数据(Schema.org)让你脱颖而出
什么是结构化数据?为什么它这么重要?
你搜过”苹果派怎么做”吗?
注意看搜索结果——有些食谱直接显示了评分(4.8 星)、烹饪时间(45 分钟)、卡路里(320kcal),甚至还有步骤预览。而有些只有标题和简介。
区别就在于结构化数据。
结构化数据是一种标准格式(JSON-LD),用来告诉搜索引擎:“这是一篇博客文章,作者是 XXX,发布于 XXX”或者”这是一个产品,价格是 XXX,评分是 XXX”。搜索引擎拿到这些信息后,就能在搜索结果中展示富媒体(Rich Snippets)——也就是那些带评分、价格、作者信息的卡片。
数据不会骗人:实现结构化数据的网站,点击率平均提升 20-30%。相当于你的流量直接涨三成,投入的成本却几乎为零。
常用的 Schema 类型
Schema.org 定义了几百种类型,但对大部分网站来说,常用的就这几种:
- Organization - 公司/组织信息(放在首页)
- BlogPosting - 博客文章(每篇文章都加)
- Product - 产品信息(电商必备,包含价格、评分、库存)
- FAQPage - 常见问题(FAQ 页面,能在搜索结果中直接展开)
- LocalBusiness - 本地商家(餐厅、理发店等,会显示地址、营业时间、电话)
我们重点讲前两个,因为这是博客和企业网站最需要的。
在 Next.js 中实现 JSON-LD
Next.js 15 的 <Script> 组件让实现结构化数据变得特别简单。我一般会创建一个通用组件:
// components/StructuredData.tsx
import Script from 'next/script'
type StructuredDataProps = {
data: object
}
export default function StructuredData({ data }: StructuredDataProps) {
return (
<Script
id="structured-data"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
)
}
然后在页面中使用:
示例 1:Organization(公司信息)
// app/layout.tsx (root layout)
import StructuredData from '@/components/StructuredData'
const organizationData = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'TechBlog',
url: 'https://yourdomain.com',
logo: 'https://yourdomain.com/logo.png',
sameAs: [
'https://twitter.com/yourusername',
'https://github.com/yourcompany',
'https://linkedin.com/company/yourcompany',
],
contactPoint: {
'@type': 'ContactPoint',
email: 'hello@yourdomain.com',
contactType: 'Customer Service',
},
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<StructuredData data={organizationData} />
</body>
</html>
)
}
示例 2:BlogPosting(博客文章)
// app/blog/[slug]/page.tsx
import StructuredData from '@/components/StructuredData'
import { getPostBySlug } from '@/lib/posts'
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
const articleData = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.coverImage,
author: {
'@type': 'Person',
name: post.author,
url: `https://yourdomain.com/author/${post.authorSlug}`,
},
publisher: {
'@type': 'Organization',
name: 'TechBlog',
logo: {
'@type': 'ImageObject',
url: 'https://yourdomain.com/logo.png',
},
},
datePublished: post.publishedAt,
dateModified: post.updatedAt,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://yourdomain.com/blog/${post.slug}`,
},
}
return (
<>
<article>{post.content}</article>
<StructuredData data={articleData} />
</>
)
}
看起来代码有点多,但其实就是把你文章的元信息用标准格式告诉 Google。配置一次,后面都是复制粘贴。
验证你的结构化数据
配完之后怎么知道对不对?用这两个工具:
-
Google Rich Results Test(https://search.google.com/test/rich-results)
- 输入你的页面 URL,Google 会告诉你能展示哪些富媒体
- 如果有错误,会明确指出哪里不对
-
Schema Markup Validator(https://validator.schema.org/)
- 检查 JSON-LD 格式是否符合 Schema.org 标准
- 比 Google 工具更严格,建议两个都测
我见过最常见的错误是忘了配 publisher(对 BlogPosting 是必填项)或者图片 URL 不是绝对路径。验证工具会直接告诉你缺什么,改完再测一次就好。
Open Graph 和 Twitter Cards 实战
社交分享为什么这么重要?
你有没有这种经历:在 Twitter 或 Facebook 分享一篇文章,结果预览区要么一片空白,要么显示的图片完全不对(比如网站 logo 或者某个随机的装饰图)?
这会让人觉得很不专业。而那些配置正确的网站,分享链接时会自动显示精美的封面图、标题、简介——点击率能高出 2-3 倍。
配置 Open Graph 和 Twitter Cards,就是为了控制你的链接在社交媒体上的展示效果。
Open Graph 协议详解
Open Graph 最初是 Facebook 制定的标准,现在被 Twitter、LinkedIn、Slack、Discord 等所有主流平台支持。
前面讲 Metadata API 时,我们其实已经配过 openGraph 对象了。但这里再详细讲一下核心字段:
export const metadata: Metadata = {
openGraph: {
// 必填字段
title: '文章标题', // 在社交媒体中显示的标题
description: '文章简介', // 简介,150字符左右
url: 'https://yourdomain.com/article', // 这篇内容的 URL
siteName: 'TechBlog', // 网站名称
// 图片(最重要!)
images: [
{
url: 'https://yourdomain.com/og-image.jpg',
width: 1200,
height: 630, // 推荐尺寸 1200x630
alt: '图片描述(无障碍和 SEO)',
},
],
// 内容类型
type: 'article', // 文章用 article,其他页面用 website
// 文章特有字段(如果 type 是 article)
publishedTime: '2025-01-15T08:00:00.000Z',
modifiedTime: '2025-01-16T10:30:00.000Z',
authors: ['张三', '李四'],
tags: ['Next.js', 'SEO', '前端开发'],
// 本地化(如果有多语言版本)
locale: 'zh_CN',
alternateLocale: ['en_US', 'ja_JP'],
},
}
关键点:图片尺寸
1200x630 像素是黄金比例(1.91:1),在所有平台都显示完美:
- Facebook、LinkedIn:完整显示
- Twitter:裁剪为 2:1 也不会太难看
- Slack、Discord:同样适用
图片大小不能超过 8MB,否则构建会失败。
Twitter Cards 配置
Twitter 有自己的 meta 标签系统,虽然也会回退使用 Open Graph,但最好单独配置:
export const metadata: Metadata = {
twitter: {
card: 'summary_large_image', // 大图模式(推荐)
site: '@yourusername', // 网站的 Twitter 账号
creator: '@authorusername', // 作者的 Twitter 账号
title: '文章标题',
description: '文章简介',
images: ['https://yourdomain.com/twitter-image.jpg'],
},
}
card 字段有两个值:
summary:小图模式(图片显示在左侧,文字在右侧)summary_large_image:大图模式(图片占满整个卡片上半部分,建议用这个)
Twitter 图片限制:大小不能超过 5MB(比 OG 更严格)。
动态生成社交分享图(进阶)
手动为每篇文章做一张 1200x630 的封面图?累死人。
Next.js 13.3 之后支持用代码动态生成 OG 图片,特别适合博客:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getPostBySlug } from '@/lib/posts'
export const runtime = 'edge'
export const alt = 'Blog post cover'
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
export default async function Image({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
return new ImageResponse(
(
<div
style={{
fontSize: 60,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
padding: '80px',
}}
>
<h1 style={{ fontSize: 72, fontWeight: 'bold', textAlign: 'center' }}>
{post.title}
</h1>
<p style={{ fontSize: 36, marginTop: 20, opacity: 0.9 }}>
by {post.author}
</p>
</div>
),
{
...size,
}
)
}
这样每篇文章的分享图都是根据标题动态生成的!不需要手动设计。
如果你想用自定义字体或背景图,可以进一步定制。具体可以看 Next.js 官方文档的 next/og 部分。
测试和验证社交分享效果
配完之后,别急着发,先用这些工具测试:
-
Facebook Sharing Debugger(https://developers.facebook.com/tools/debug/)
- 输入 URL,看 Facebook 会怎么抓取和显示
- 注意:Facebook 会缓存 OG 信息,如果改了代码,需要在这个工具里点”Scrape Again”刷新缓存
-
Twitter Card Validator(https://cards-dev.twitter.com/validator)
- 输入 URL,预览 Twitter 卡片效果
- 注:2023 年后这个工具需要 Twitter 开发者账号才能用,但可以直接在 Twitter 发推测试
-
LinkedIn Post Inspector(https://www.linkedin.com/post-inspector/)
- LinkedIn 的预览工具,同样可以刷新缓存
我踩过的坑:改完 OG 图片后,Facebook 还是显示旧图。一定要去 Sharing Debugger 刷新缓存,不然你会怀疑人生。
其他 SEO 必备配置
前面讲的 Metadata API、结构化数据、Open Graph 是 SEO 的核心,但还有几个配置同样重要,别漏了。
sitemap.xml - 让搜索引擎知道你有哪些页面
Sitemap 是一个 XML 文件,列出了你网站的所有页面 URL。Google 和 Bing 的爬虫会读这个文件,知道你的网站结构,加快索引速度。
Next.js App Router 让这件事变得超简单——你只需要创建一个 app/sitemap.ts:
// app/sitemap.ts
import { MetadataRoute } from 'next'
import { getAllPosts } from '@/lib/posts'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts()
const baseUrl = 'https://yourdomain.com'
// 静态页面
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
]
// 动态生成博客文章页面
const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.7,
}))
return [...staticPages, ...blogPages]
}
Next.js 会自动在 https://yourdomain.com/sitemap.xml 生成这个文件。
配置后记得做这两件事:
- 提交到 Google Search Console(https://search.google.com/search-console)
- 提交到 Bing Webmaster Tools(https://www.bing.com/webmasters)
提交后,新页面的索引速度能提升 50% 以上。我以前写博客,不提交 sitemap,新文章要两周才被索引;提交后,三天就能在 Google 搜到。
robots.txt - 控制爬虫的访问权限
robots.txt 告诉搜索引擎爬虫”哪些页面可以抓,哪些不能抓”。
同样,Next.js App Router 支持用代码生成:
// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*', // 对所有爬虫生效
allow: '/', // 允许抓取所有页面
disallow: ['/admin', '/api', '/private'], // 禁止抓这些路径
},
],
sitemap: 'https://yourdomain.com/sitemap.xml', // 指向 sitemap
}
}
这会生成 https://yourdomain.com/robots.txt,内容大概是:
User-agent: *
Allow: /
Disallow: /admin
Disallow: /api
Disallow: /private
Sitemap: https://yourdomain.com/sitemap.xml
常见场景:
- 后台管理页面(
/admin)肯定不想被搜索引擎抓 - API 路由(
/api)也没必要抓 - 草稿、预览页面可以用
noindexmeta 标签或Disallow规则
canonical URL - 避免重复内容惩罚
Canonical URL 是告诉搜索引擎:“这个页面有多个 URL,但这才是正宗的”。
典型场景:
- 分页:
/blog?page=1、/blog?page=2 - 筛选:
/products?category=tech - 排序:
/products?sort=price
这些其实是同一个页面的不同视图,如果不声明 canonical,搜索引擎会认为它们是重复内容,分散权重。
在 Next.js 中配置 canonical:
// app/blog/page.tsx
export const metadata: Metadata = {
alternates: {
canonical: 'https://yourdomain.com/blog',
},
}
或者动态生成:
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
return {
alternates: {
canonical: `https://yourdomain.com/blog/${params.slug}`,
},
}
}
如果你的页面有多语言版本,还可以用 alternates.languages 告诉 Google:
export const metadata: Metadata = {
alternates: {
canonical: 'https://yourdomain.com/blog/nextjs-seo',
languages: {
'en-US': 'https://yourdomain.com/en/blog/nextjs-seo',
'ja-JP': 'https://yourdomain.com/ja/blog/nextjs-seo',
},
},
}
图片优化 - next/image + alt 属性
很多开发者忽略图片 SEO,但其实 Google 图片搜索是一个巨大的流量入口。
两个关键点:
- 用
next/image替代<img>
import Image from 'next/image'
<Image
src="/cover.jpg"
alt="Next.js SEO 完全指南封面"
width={1200}
height={630}
priority // 首屏图片加这个,优先加载
/>
next/image 会自动做这些优化:
- 懒加载(首屏外的图片延迟加载)
- WebP 格式转换(体积更小)
- 响应式尺寸(根据设备加载合适大小)
- 防止 CLS(累积布局偏移,Core Web Vitals 指标之一)
- 必须写
alt属性
alt 不是可选的,它既是 SEO 需求,也是无障碍需求(屏幕阅读器会读 alt)。
好的 alt 描述:
- ✅ “Next.js Metadata API 配置代码示例”
- ✅ “博客文章在 Google 搜索结果中的富媒体展示”
糟糕的 alt 描述:
- ❌ “图片”
- ❌ “screenshot.png”
- ❌ 不写 alt
Google 图片搜索会根据 alt 内容给图片排名。我见过一个设计素材网站,30% 的流量来自图片搜索,就是因为他们认真写了每张图的 alt。
实战案例 - 博客网站 SEO 完整配置
前面讲了那么多理论和代码片段,现在把它们整合起来,看一个完整的博客项目应该怎么配置 SEO。
项目结构
假设我们用 Next.js 15 App Router 做一个技术博客,目录结构大概是这样:
app/
├── layout.tsx # Root layout - 全局配置
├── page.tsx # 首页
├── about/page.tsx # 关于页面
├── blog/
│ ├── page.tsx # 博客列表页
│ └── [slug]/
│ ├── page.tsx # 博客详情页
│ └── opengraph-image.tsx # 动态生成 OG 图片(可选)
├── sitemap.ts # Sitemap 生成
└── robots.ts # Robots.txt 生成
完整代码示例
1. Root Layout - 全局 SEO 配置
// app/layout.tsx
import { Metadata } from 'next'
import StructuredData from '@/components/StructuredData'
export const metadata: Metadata = {
metadataBase: new URL('https://yourdomain.com'), // 必须配置,用于拼接相对路径
title: {
template: '%s | TechBlog', // 子页面标题模板
default: 'TechBlog - 前端开发技术博客',
},
description: '分享 Next.js、React、TypeScript 实战经验的技术博客',
keywords: ['Next.js', 'React', 'TypeScript', '前端开发', '技术博客'],
authors: [{ name: '张三', url: 'https://yourdomain.com/about' }],
openGraph: {
type: 'website',
siteName: 'TechBlog',
locale: 'zh_CN',
},
twitter: {
card: 'summary_large_image',
site: '@yourusername',
},
}
// Organization 结构化数据(全局,放在 root layout)
const organizationData = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'TechBlog',
url: 'https://yourdomain.com',
logo: 'https://yourdomain.com/logo.png',
sameAs: [
'https://twitter.com/yourusername',
'https://github.com/yourcompany',
],
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>
{children}
<StructuredData data={organizationData} />
</body>
</html>
)
}
2. 首页 - 静态 metadata
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: {
absolute: 'TechBlog - 前端开发技术博客', // 不套用 template
},
description: '分享 Next.js、React、TypeScript 等前端技术实战经验,帮助开发者提升技能',
openGraph: {
title: 'TechBlog - 前端开发技术博客',
description: '分享前端技术实战经验',
url: 'https://yourdomain.com',
images: [
{
url: 'https://yourdomain.com/og-home.jpg',
width: 1200,
height: 630,
alt: 'TechBlog 首页封面',
},
],
},
}
export default function HomePage() {
return <div>首页内容...</div>
}
3. 博客详情页 - 动态 metadata + 结构化数据
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPostBySlug } from '@/lib/posts'
import StructuredData from '@/components/StructuredData'
// 动态生成 metadata
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const post = await getPostBySlug(params.slug)
if (!post) return {}
return {
title: post.title, // 会套用 template,变成 "文章标题 | TechBlog"
description: post.excerpt,
keywords: post.tags,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
url: `https://yourdomain.com/blog/${post.slug}`,
images: [post.coverImage],
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
alternates: {
canonical: `https://yourdomain.com/blog/${post.slug}`,
},
}
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
if (!post) notFound()
// BlogPosting 结构化数据
const articleData = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.coverImage,
datePublished: post.publishedAt,
dateModified: post.updatedAt || post.publishedAt,
author: {
'@type': 'Person',
name: post.author,
},
publisher: {
'@type': 'Organization',
name: 'TechBlog',
logo: {
'@type': 'ImageObject',
url: 'https://yourdomain.com/logo.png',
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://yourdomain.com/blog/${post.slug}`,
},
}
return (
<>
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
<StructuredData data={articleData} />
</>
)
}
4. Sitemap 和 Robots
// app/sitemap.ts
import { getAllPosts } from '@/lib/posts'
export default async function sitemap() {
const posts = await getAllPosts()
const baseUrl = 'https://yourdomain.com'
const blogUrls = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.7,
}))
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 1,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
...blogUrls,
]
}
// app/robots.ts
export default function robots() {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/api', '/admin'],
},
sitemap: 'https://yourdomain.com/sitemap.xml',
}
}
部署后的验证清单
配完之后,上线前一定要检查:
-
查看 HTML 源码
- 在浏览器按 F12 → Elements → 查看
<head>标签 - 确认
<title>、<meta name="description">、OG 标签都正确渲染
- 在浏览器按 F12 → Elements → 查看
-
测试 sitemap 和 robots
- 访问
https://yourdomain.com/sitemap.xml,看是否正常生成 - 访问
https://yourdomain.com/robots.txt,检查内容
- 访问
-
验证结构化数据
- 用 Google Rich Results Test 测试几个页面
- 确认没有错误或警告
-
测试社交分享
- 用 Facebook Sharing Debugger 和 Twitter Card Validator 测试
- 确认图片、标题、描述都正确显示
-
提交到搜索引擎
- Google Search Console 提交 sitemap
- Bing Webmaster Tools 提交 sitemap
做完这些,你的 Next.js 网站 SEO 就算配置完整了。
结论
如果你认真读到这里,应该已经明白:Next.js 的 SSR 不等于 SEO 友好,但 Next.js 15 提供的工具确实让 SEO 配置变得简单得多。
回顾一下核心要点:
- Metadata API 让你用类型安全的方式配置 meta 标签,静态页面用 metadata 对象,动态页面用 generateMetadata 函数
- 结构化数据(JSON-LD) 是点击率提升 20-30% 的秘密武器,用
<Script>组件就能轻松实现 - Open Graph 和 Twitter Cards 决定了你的链接在社交媒体上的展示效果,图片尺寸 1200x630 是万能的
- sitemap.xml 和 robots.txt 用
.ts文件就能自动生成,别忘了提交到 Search Console - 图片优化 用
next/image+ 认真写 alt,图片搜索也能带来可观流量
说实话,这些配置看起来繁琐,但投入产出比极高。我见过太多产品技术很强,就是因为 SEO 没做好,流量始终上不去,只能靠烧钱做推广。而那些配置正确的网站,自然流量源源不断,成本几乎为零。
SEO 不是玄学,是科学。按照本文的清单逐项配置,用工具验证,效果一定会显现——也许不是立竿见影,但三个月后你会看到明显的流量增长。
别等流量彻底没了才想起 SEO。现在就打开你的项目,花半天时间配置一下。如果遇到问题,把本文收藏起来,随时查阅。
如果这篇文章对你有帮助,不妨分享给其他开发者朋友——帮他们少踩几个坑,也算功德一件。
"},{"@type":"HowToStep","position":4,"name":"配置sitemap和robots.txt","text":"创建sitemap.ts:\n• 导出default函数返回sitemap数组\n• 包含所有页面的URL、lastModified、changeFrequency\n• 支持动态生成\n\n创建robots.ts:\n• 配置允许/禁止的爬虫\n• 设置sitemap路径\n• 配置爬取规则"},{"@type":"HowToStep","position":5,"name":"优化图片SEO","text":"图片优化要点:\n• 使用next/image组件\n• 添加有意义的alt文本\n• 配置图片尺寸(1200x630适合OG图片)\n• 使用WebP/AVIF格式\n• 添加图片结构化数据(ImageObject)"},{"@type":"HowToStep","position":6,"name":"验证和测试","text":"验证工具:\n• Google Rich Results Test:验证结构化数据\n• Facebook Sharing Debugger:测试OG标签\n• Twitter Card Validator:测试Twitter Cards\n• Google Search Console:提交sitemap并监控\n\n检查清单:\n• 每个页面都有独特的title和description\n• OG图片尺寸正确(1200x630)\n• 结构化数据格式正确\n• sitemap已提交到Search Console"}],"totalTime":"PT4H"}Next.js SEO 优化完整配置流程
从Metadata API配置到结构化数据、sitemap、robots.txt的完整SEO优化步骤
⏱️ 预计耗时: 4 小时
- 1
步骤1: 配置基础Metadata
使用Next.js 15 Metadata API:
• 在layout.js或page.js中导出metadata对象
• 配置title、description、keywords
• 设置Open Graph和Twitter Cards
示例:
export const metadata = {
title: '页面标题',
description: '页面描述',
openGraph: {
title: 'OG标题',
description: 'OG描述',
images: ['/og-image.jpg']
}
} - 2
步骤2: 配置动态Metadata
对于动态路由:
• 使用generateMetadata函数
• 根据路由参数生成metadata
• 支持async函数获取数据
示例:
export async function generateMetadata({ params }) {
const post = await getPost(params.id)
return {
title: post.title,
description: post.description
}
} - 3
步骤3: 添加结构化数据
使用JSON-LD格式:
• 创建script标签添加JSON-LD
• 支持Article、Product、FAQ等类型
• 使用Schema.org词汇表
示例:
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "Article",
"headline": "文章标题"
})}
</script> - 4
步骤4: 配置sitemap和robots.txt
创建sitemap.ts:
• 导出default函数返回sitemap数组
• 包含所有页面的URL、lastModified、changeFrequency
• 支持动态生成
创建robots.ts:
• 配置允许/禁止的爬虫
• 设置sitemap路径
• 配置爬取规则 - 5
步骤5: 优化图片SEO
图片优化要点:
• 使用next/image组件
• 添加有意义的alt文本
• 配置图片尺寸(1200x630适合OG图片)
• 使用WebP/AVIF格式
• 添加图片结构化数据(ImageObject) - 6
步骤6: 验证和测试
验证工具:
• Google Rich Results Test:验证结构化数据
• Facebook Sharing Debugger:测试OG标签
• Twitter Card Validator:测试Twitter Cards
• Google Search Console:提交sitemap并监控
检查清单:
• 每个页面都有独特的title和description
• OG图片尺寸正确(1200x630)
• 结构化数据格式正确
• sitemap已提交到Search Console
常见问题
SSR 和 SEO 有什么关系?
Metadata API 和 Head 组件有什么区别?
如何为动态路由配置metadata?
例如:
export async function generateMetadata({ params }) {
const data = await getData(params.id)
return { title: data.title }
}
Open Graph 图片尺寸应该多大?
结构化数据是必须的吗?
sitemap 和 robots.txt 需要手动创建吗?
SEO 优化后多久能看到效果?
建议:
1) 提交sitemap到Google Search Console
2) 使用Google Rich Results Test验证
3) 持续监控Search Console数据
4) 保持内容更新
17 分钟阅读 · 发布于: 2025年12月19日 · 修改于: 2026年1月22日
相关文章
Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 单元测试实战:Jest + React Testing Library 完整配置指南

评论
使用 GitHub 账号登录后即可评论