
一句话:SEO 的四根支柱是「可抓取的内容(SSR/SSG)、准确的元数据(title/description/OG)、结构化数据(JSON-LD)、以及「Core Web Vitals 性能」;Next.js 的
metadataAPI 让元数据可以「声明式 + 动态」生成。
一、SEO 的四个要素
| 要素 | 作用 | 做法 |
|---|---|---|
| 可抓取的内容 | 爬虫能读到真实内容 | SSR / SSG / ISR(而不是「纯 CSR 空 HTML」) |
| 元数据 | 决定「搜索结果的标题与摘要」「分享卡片」 | <title>、description、OG、Twitter Card |
| 结构化数据 | 让搜索引擎理解内容语义(富摘要) | JSON-LD(Schema.org) |
| 性能 | Core Web Vitals 影响排名与体验 | LCP / INP / CLS(见 08 章的优化) |
⚠️ 三个常见误解:
① 「SEO 就是堆关键词」——现代搜索引擎更看「内容质量 + 结构化 + 性能」
② 「用 SPA 也能被收录」——能执行 JS 的爬虫有限(且渲染超时会丢内容),
所以「关键内容用 SSR/SSG 输出」仍然是主流做法
③ 「meta 只要 title 和 description」——分享卡片(OG)与结构化数据同样重要
二、metadata API(Next.js)
// ✅ 静态元数据(导出 metadata 对象)
// app/blog/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '博客 | 我的站点',
description: '分享前端与工程实践',
keywords: ['前端', 'React', '工程化'],
alternates: { canonical: 'https://example.com/blog' }, // ⭐ 规范链接
openGraph: {
title: '博客 | 我的站点',
description: '分享前端与工程实践',
url: 'https://example.com/blog',
siteName: '我的站点',
images: [{ url: '/og/blog.png', width: 1200, height: 630 }],
type: 'website',
locale: 'zh_CN',
},
twitter: {
card: 'summary_large_image',
title: '博客 | 我的站点',
images: ['/og/blog.png'],
},
robots: { index: true, follow: true },
};
// ✅ 动态元数据(generateMetadata)—— 依赖路由参数/数据
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return { title: '文章未找到' };
}
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: `/api/og?title=${encodeURIComponent(post.title)}`, width: 1200, height: 630 }],
},
alternates: { canonical: `https://example.com/blog/${slug}` },
};
}
// ✅ 根布局设置「模板」(子页面只需给「后缀」)
// app/layout.tsx
export const metadata: Metadata = {
title: {
default: '我的站点',
template: '%s | 我的站点', // ⭐ 子页面 title 会套进模板
},
description: '站点默认描述',
};
// app/blog/page.tsx
export const metadata: Metadata = { title: '博客' };
// → 实际输出 <title>博客 | 我的站点</title>
| 字段 | 作用 |
|---|---|
title |
搜索结果标题(最重要) |
description |
搜索结果摘要(影响点击率) |
openGraph |
社交平台分享卡片 |
twitter |
Twitter/X 的卡片 |
alternates.canonical |
规范链接(避免重复内容被分散权重) |
robots |
是否允许索引/跟随 |
icons |
favicon |
metadataBase |
让相对 URL(图片)解析为绝对 URL(OG 图必须是绝对 URL) |
// ⚠️ 必须设置 metadataBase(否则 OG 图的相对路径无法被爬虫解析)
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'),
openGraph: { images: ['/og.png'] }, // → 解析成 https://example.com/og.png
};
三、分享卡片(Open Graph)
分享卡片的三要素(缺一不可):
① og:title / og:description —— 标题与描述
② og:image —— 图片(建议 1200×630,**必须是绝对 URL**)
③ og:url —— 页面规范地址
⭐ 为什么重要?
· 微信/飞书/Twitter/Facebook/Slack 的「链接预览」都读 OG
· 如果 og:image 缺失或不可访问 → 卡片显示「无图」(点击率大幅下降)
· 如果是 CSR 动态插入 meta → 这些平台的爬虫【读不到】(它们不执行 JS)
→ 所以「分享卡片必须在服务端渲染的 HTML 里」
// ✅ 动态生成 OG 图(Next.js 的 ImageResponse)
// app/api/og/route.tsx
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get('title') ?? '默认标题';
return new ImageResponse(
(
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: '100%',
height: '100%',
padding: 80,
background: '#0f172a',
color: 'white',
fontSize: 64,
}}
>
<div>{title}</div>
<div style={{ fontSize: 32, marginTop: 24, opacity: 0.7 }}>example.com</div>
</div>
),
{ width: 1200, height: 630 }
);
}
// 在 generateMetadata 里引用它
openGraph: {
images: [`/api/og?title=${encodeURIComponent(post.title)}`],
}
⚠️ OG 图的四个注意点:
① 尺寸 1200×630(比例 1.91:1)——这是各平台的主流规格
② **必须绝对 URL**(爬虫在别的域名下请求,相对路径无法解析)
③ 图片要「可公开访问」(不能需要登录/防盗链)
④ 更新后平台有缓存(微信/Feishu 会缓存一段时间,可用「调试工具」刷新)
四、结构化数据(JSON-LD)
// ✅ 在页面里注入 JSON-LD(Schema.org 的结构化数据)
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: `https://example.com/og/${slug}.png`,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: { '@type': 'Person', name: post.author.name },
publisher: {
'@type': 'Organization',
name: '我的站点',
logo: { '@type': 'ImageObject', url: 'https://example.com/logo.png' },
},
mainEntityOfPage: { '@type': 'WebPage', '@id': `https://example.com/blog/${slug}` },
};
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} // ⚠️ 用 JSON.stringify 而不是手拼
/>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
⭐ 常用类型(不同页面用不同 @type):
· 文章:BlogPosting / Article / NewsArticle
· 商品:Product(含 offers:price、priceCurrency、availability)
· 面包屑:BreadcrumbList
· 组织:Organization(首页 + logo)
· 问答:FAQPage(可产生「常见问题」富摘要)
· 视频:VideoObject
⚠️ JSON-LD 的四个注意点:
① 必须与「页面实际内容一致」(虚标会被惩罚)
② 用 JSON.stringify 生成(不要手拼字符串,容易转义出错)
③ 放在页面里「服务端渲染的 HTML」中(CSR 注入的爬虫读不到)
④ 可以用「富媒体测试工具」(Google Rich Results Test)验证
五、sitemap / robots / canonical
// ✅ 动态 sitemap(Next.js)
// app/sitemap.ts
import type { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts();
return [
{ url: 'https://example.com', lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
{ url: 'https://example.com/blog', lastModified: new Date(), changeFrequency: 'daily', priority: 0.8 },
...posts.map((p) => ({
url: `https://example.com/blog/${p.slug}`,
lastModified: new Date(p.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.6,
})),
];
}
// ✅ robots.txt
// app/robots.ts
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: '*', allow: '/', disallow: ['/admin/', '/api/'] },
],
sitemap: 'https://example.com/sitemap.xml',
};
}
⭐ 三者的作用:
· sitemap.xml —— 告诉爬虫「有哪些 URL 需要抓」(新站/内容站必备)
· robots.txt —— 告诉爬虫「哪些不该抓」(后台、接口、搜索结果页)
· canonical —— 告诉搜索引擎「哪个 URL 是「正版」」(避免参数页/多域名分散权重)
⚠️ canonical 的常见坑:
· 带查询参数的页面(?page=2、?utm_source=...)都要指向「不带参数的规范 URL」
· 多个域名(如 www 与不带 www)要统一到一个
· «分页的第 2 页» 的 canoncaical 应该指向自己(不要都指向第 1 页)
六、性能与 Core Web Vitals
Core Web Vitals 是「排名因素」之一(也是体验指标):
① LCP(Largest Contentful Paint)——「主要内容多快出现」
· 目标 < 2.5s
· 优化:SSR/SSG 输出关键内容、图片优化(next/image)、避免阻塞渲染的资源
② INP(Interaction to Next Paint)——「交互响应多快」
· 目标 < 200ms
· 优化:减少主线程长任务、代码分割、虚拟滚动、useTransition(见 05 章)
③ CLS(Cumulative Layout Shift)——「布局稳定性」
· 目标 < 0.1
· 优化:图片/骨架**设定宽高**、字体 `font-display: swap` + 预留空间、
广告/嵌入内容预留位置
⭐ 注意:SEO 不只是「元数据」,性能差的页面排名与点击率都会受影响。
// ✅ next/image:自动优化(尺寸、格式、懒加载、避免 CLS)
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="产品截图"
width={1200}
height={630} // ⭐ 必须给宽高(避免布局跳动)
priority // ⭐ 首屏关键图片:优先加载(提升 LCP)
/>;
七、常见坑
// ① 用纯 CSR 渲染「SEO 关键内容」(爬虫看到空 HTML)
// ② OG 图用「相对路径」(爬虫无法解析 → 分享无图)
openGraph: { images: ['/og.png'] }; // ⚠️ 需要配置 metadataBase
openGraph: { images: ['https://example.com/og.png'] }; // ✅ 绝对 URL
// ③ 忘记 metadataBase(相对图片 URL 无法解析)
// ④ 用「客户端 JS 动态改 title/meta」(分享爬虫读不到)
useEffect(() => { document.title = post.title; }, []); // ⚠️ 爬虫不执行 JS
// ✅ 用 generateMetadata(服务端渲染进 HTML)
// ⑤ 所有页面用「同一个 title」(无法区分、影响排名)
// ⑥ description 太长(会被截断)或为空(搜索引擎自己摘取)
// ⑦ JSON-LD 与页面内容不一致(虚标 → 被惩罚)
// ⑧ 手拼 JSON-LD 字符串(转义错误)→ 用 JSON.stringify
// ⑨ 忘记 canonical(参数页/多域名分散权重)
// ⑩ 忘记 sitemap(新内容抓取慢)
// ⑪ robots.txt 屏蔽了「不该屏蔽的」(如整站、CSS/JS 资源)
// ⚠️ 屏蔽 CSS/JS 会让爬虫无法正确渲染页面 → 影响收录
// ⑫ 用 ISR 渲染「内容因人而异」的页面(缓存串用户 + 影响 SEO)
// ⑬ 图片没有宽高 → CLS 高(影响排名)
// ⑭ 用「文字图片」代替真实文本(爬虫读不到)
// ⑮ 忽略「国际化 SEO」(hreflang、多语言 canonical)
// ⑯ 认为「SSR 就一定 SEO 好」(还要有正确的 meta、可抓取的结构、性能)
// ⑰ 忘记「推送 sitemap 到搜索引擎」(Google Search Console / Bing)
// ⑱ 用「hash 路由」(#/path)做内容页(爬虫对 hash 的处理不一致)
// ④ 的正确做法对比
// ❌ 客户端改 title(分享卡片抓不到)
'use client';
useEffect(() => { document.title = post.title; }, [post.title]);
// ✅ 服务端生成元数据(进 HTML)
export async function generateMetadata({ params }) {
const post = await getPost((await params).slug);
return { title: post.title, description: post.excerpt };
}
八、面试延伸
- 「前端怎么做 SEO?」
四件事:① 让内容可被抓取——用 SSR / SSG / ISR 输出「带真实内容的 HTML」(纯 CSR 的「空 HTML」爬虫读不到内容);② 准确的元数据——title(最重要)、description、Open Graph(分享卡片)、canonical(避免重复内容);③ 结构化数据(JSON-LD)——让搜索引擎理解内容语义(文章、商品、问答等,可产生富摘要);④ 性能——Core Web Vitals(LCP/INP/CLS)既是体验指标也是排名因素。 -
「为什么分享卡片必须用 SSR?」
因为社交平台的爬虫(微信、飞书、Twitter、Facebook、Slack)通常不执行 JavaScript——它们只读取「服务端返回的 HTML 里的 meta 标签」。如果og:title、og:image是客户端 JS 动态插入的,爬虫看到的是「空的 HTML」→ 卡片就没有标题/图片。所以「分享卡片相关元数据必须在服务端渲染的 HTML 里」(Next.js 的generateMetadata就是为此设计的)。 -
「
generateMetadata和metadata有什么区别?」
①export const metadata——静态元数据对象(在模块加载时确定),适合「不依赖路由参数/数据」的页面;②export async function generateMetadata({ params, searchParams })——动态生成(可以是async,能根据路由参数查数据),适合「详情页」(title/description/OG 图都依赖具体内容)。两者不能同时导出(会报错),并且都只在 Server Component 里可用。 -
「OG 图为什么必须是绝对 URL?」
因为分享平台/爬虫是在「自己的服务器上」抓取并渲染卡片的——它们拿到的是「一个 URL 字符串」,然后去请求这张图。如果og:image是相对路径(/og.png),爬虫无法知道「相对于哪个域名」(它并不知道你的页面部署在哪)。所以必须是完整的绝对 URL(https://example.com/og.png)。Next.js 里通过metadataBase让「相对路径被解析成绝对 URL」(避免在每个页面手写域名)。 -
「结构化数据(JSON-LD)有什么用?」
它让搜索引擎理解内容的语义——告诉它「这是一个文章(BlogPosting,含作者/发布时间)」「这是一个商品(Product,含价格/库存)」「这是一组问答(FAQPage)」。有了它,搜索结果可以显示「富摘要」(评分星级、价格、FAQ 折叠、面包屑),从而提高点击率。注意:① 内容必须与页面实际一致(虚标会被惩罚);② 用JSON.stringify生成(避免转义错误);③ 要放在「服务端渲染的 HTML」里(CSR 注入的读不到)。 -
「
canonical是干什么的?」
canonical(规范链接)告诉搜索引擎「哪个 URL 是这个内容的「正版」」。它解决的问题是「同一内容有多个 URL」导致的权重分散:① 带查询参数的地址(?page=1、?utm_source=xxx);② 多个域名(www与不带www、http 与 https);③ 分页/排序/筛选产生的变体。注意:分页中的「第 2 页」的 canonical 应该指向自己(不要都指向第 1 页,否则爬虫可能不去抓第 2 页内容)。 -
「Core Web Vitals 和 SEO 有什么关系?」
它们是「排名因素之一」(Google 已确认页面体验纳入排名),同时也是「用户体验指标」:① LCP(最大内容绘制)——主要内容多快出现(目标 < 2.5s);② INP(交互到下次绘制)——交互响应多快(目标 < 200ms);③ CLS(累积布局偏移)——布局是否稳定(目标 < 0.1)。优化手段与「性能优化」完全一致(SSR/SSG 输出关键内容、图片优化、减少长任务、给图片与骨架设定宽高避免 CLS)。 -
「为什么不要用
hash路由做内容页?」
因为「#之后的部分不会发送给服务器」——https://example.com/#/blog/1中的/blog/1只在浏览器端解析。这带来三个问题:① 服务器无法针对不同内容返回不同的 HTML(所有 hash 路由都拿到同一个空 HTML)→ SEO 极差;② 分享/爬虫 看到的都是同一个 URL 的内容;③ 某些平台/爬虫对 hash 的处理不一致。内容页应该用 History 路由(/blog/1)配合 SSR/SSG 输出各自的 HTML。
一句话速记
SEO 四支柱:可抓取内容(SSR/SSG/ISR,不要纯 CSR 空 HTML) + 元数据(
title/description/OG/canonical) + 结构化数据(JSON-LD,用JSON.stringify且与内容一致) + Core Web Vitals(LCP/INP/CLS);Next.js 用metadata(静态)与generateMetadata(动态、可 async),配合metadataBase让相对图片解析成绝对 URL(OG 图必须绝对 URL、1200×630、可公开访问);分享卡片必须在服务端渲染的 HTML 里(社交爬虫不执行 JS);sitemap + robots + canonical 三件套别忘(别屏蔽 CSS/JS);图片给宽高避免 CLS;别用 hash 路由做内容页(#之后不发给服务器)。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!