Next.js SEO 优化有哪些关键做法?
Next.js 的 SEO 优化,重点不是在页面里堆 keywords,而是让搜索引擎稳定拿到可理解、可分享、加载快的 HTML。老项目可能还在 Pages Router,用 next/head 管页面头部;新项目更建议用 App Router 的 Metadata API、generateMetadata、sitemap.ts 和 robots.ts 统一处理。
如果只做一件事,先保证每个可索引页面都有准确的标题、描述、canonical、结构化数据和可预渲染的正文。搜索引擎能读懂页面,用户点进来后也能快速打开,SEO 才有基础。
元数据:新项目优先用 Metadata API
App Router 里不再建议到处手写 <Head>。站点级信息放在 app/layout.tsx,页面级信息由各页面覆盖。这样标题模板、OG、Twitter、robots、canonical 等配置更集中,也不容易漏。
tsx// app/layout.tsx import type { Metadata } from 'next'; export const metadata: Metadata = { metadataBase: new URL('https://example.com'), title: { default: 'Example', template: '%s | Example', }, description: 'Example 提供前端工程与 Web 开发内容', openGraph: { type: 'website', siteName: 'Example', locale: 'zh_CN', url: '/', images: [{ url: '/og.png', width: 1200, height: 630, alt: 'Example' }], }, twitter: { card: 'summary_large_image', images: ['/og.png'], }, robots: { index: true, follow: true, googleBot: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1, }, }, }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="zh-CN"> <body>{children}</body> </html> ); }
这里故意没有把 keywords 当重点。现代搜索引擎不会因为 meta keywords 写得多就给更高排名,甚至会把堆词看成低质量信号。关键词应该自然出现在标题、首段、正文小标题、图片 alt 和链接上下文里;如果团队内部需要标签,可以保留少量 keywords,但不要指望它解决 SEO。
Pages Router 旧项目仍可用 Head
如果项目还在 pages/ 目录,next/head 仍然能用。重点是每个页面只输出一组准确的 title、description、canonical、OG 和 Twitter 信息,避免多个组件重复写同一类标签。
tsximport Head from 'next/head'; export default function BlogPost({ post }) { const url = `https://example.com/blog/${post.slug}`; return ( <> <Head> <title>{post.title} | Example</title> <meta name="description" content={post.excerpt} /> <link rel="canonical" href={url} /> <meta property="og:title" content={post.title} /> <meta property="og:description" content={post.excerpt} /> <meta property="og:image" content={post.image} /> <meta property="og:type" content="article" /> <meta property="og:url" content={url} /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content={post.title} /> <meta name="twitter:description" content={post.excerpt} /> <meta name="twitter:image" content={post.image} /> </Head> <article>{post.content}</article> </> ); }
动态页面用 generateMetadata 补全搜索结果信息
博客详情、商品详情、文档详情这类动态路由,不能只依赖默认 metadata。搜索结果里显示什么,社交软件里展开什么,通常都来自当前数据。
tsx// app/blog/[slug]/page.tsx import type { Metadata } from 'next'; async function getPost(slug: string) { const res = await fetch(`https://api.example.com/posts/${slug}`, { next: { revalidate: 3600 }, }); if (!res.ok) return null; return res.json(); } export async function generateMetadata( { params }: { params: { slug: string } } ): Promise<Metadata> { const post = await getPost(params.slug); if (!post) return { title: '文章不存在' }; const url = `/blog/${post.slug}`; return { title: post.title, description: post.excerpt, alternates: { canonical: url }, openGraph: { title: post.title, description: post.excerpt, url, type: 'article', publishedTime: post.publishedAt, modifiedTime: post.updatedAt, authors: [post.author.name], images: [{ url: post.cover, width: 1200, height: 630, alt: post.title }], }, twitter: { card: 'summary_large_image', title: post.title, description: post.excerpt, images: [post.cover], }, }; }
description 不一定直接影响排名,但会影响搜索结果摘要和点击率。写法上要像一句正常的内容摘要,别塞一串同义词。
JSON-LD 让搜索引擎更容易理解页面
结构化数据适合文章、产品、课程、面包屑、组织信息等页面。Next.js 里常见做法是在页面组件中输出 JSON-LD。不要虚构评分、作者、发布时间;结构化数据必须和页面可见内容一致。
tsx// app/blog/[slug]/page.tsx export default async function BlogPostPage({ params }: { params: { slug: string } }) { const post = await getPost(params.slug); const jsonLd = { '@context': 'https://schema.org', '@type': 'BlogPosting', headline: post.title, description: post.excerpt, image: post.cover, author: { '@type': 'Person', name: post.author.name }, datePublished: post.publishedAt, dateModified: post.updatedAt, mainEntityOfPage: `https://example.com/blog/${post.slug}`, }; return ( <article> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> <h1>{post.title}</h1> <p>{post.excerpt}</p> </article> ); }
JSON-LD 不是排名捷径,它更像给搜索引擎的说明书。内容本身薄,结构化数据写得再完整也救不了。
SSR、SSG、ISR 要按内容变化频率选
Next.js SEO 的优势在于预渲染。搜索引擎拿到的是完整 HTML,而不是等待浏览器执行一堆客户端 JavaScript 后才出现正文。
- SSG:适合文档、博客、营销页。内容构建时已知,访问速度快,缓存友好。
- ISR:适合会更新但不要求秒级实时的内容,比如文章、商品列表、公开资料页。
- SSR:适合强实时内容,比如库存、价格、个性化页面,但要控制接口耗时,否则 TTFB 会拖累体验。
App Router 中可以用 revalidate 或 fetch 的 next.revalidate 控制 ISR:
tsxexport const revalidate = 3600; export default async function Page() { const posts = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 }, }).then((res) => res.json()); return <PostList posts={posts} />; }
Pages Router 里的 getStaticProps、getServerSideProps、getStaticPaths 仍然是旧项目的核心选择。迁移到 App Router 后,对应思路变成服务器组件、fetch 缓存、generateStaticParams 和路由段配置。
动态路由要让重要页面提前可访问
列表页能被抓到,不代表详情页一定能被抓到。动态路由需要稳定的 URL、可发现的内部链接,以及可生成的静态参数。
tsx// app/blog/[slug]/page.tsx export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts').then((res) => res.json()); return posts.map((post) => ({ slug: post.slug })); } export const dynamicParams = true;
对于内容量很大的网站,不必一次生成所有详情页。可以先生成核心页面,再用 ISR 补齐长尾页面。需要注意的是,低价值、重复或空内容页面不要放进 sitemap,也不要让站内到处链接过去。
语义化 HTML 比花哨 DOM 更有用
搜索引擎和辅助技术都依赖清晰结构。文章页应使用 article、header、time、section、nav、footer 等语义标签,标题层级不要跳来跳去。页面只有一个 H1,正文区域从 H2 往下组织更稳。
tsx<article> <header> <h1>{post.title}</h1> <time dateTime={post.publishedAt}>{post.publishedAtText}</time> </header> <section aria-labelledby="summary-title"> <h2 id="summary-title">核心观点</h2> <p>{post.summary}</p> </section> <footer> <p>作者:{post.author.name}</p> </footer> </article>
站内链接也要写清楚。了解更多 这种锚文本信息量太低,查看 Next.js Metadata API 示例 会更明确。
图片用 next/image,同时写好 alt 和尺寸
图片会影响 LCP、CLS 和图片搜索流量。next/image 能处理懒加载、响应式尺寸和格式优化,但前提是你给出正确的宽高、sizes 和描述性 alt。
tsximport Image from 'next/image'; export function CoverImage({ post }) { return ( <Image src={post.cover} alt={`${post.title} 封面图`} width={1200} height={630} sizes="(max-width: 768px) 100vw, 768px" priority /> ); }
首屏主图可以用 priority,正文里的普通图片交给默认懒加载。不要为了 SEO 在 alt 里重复堆关键词,描述图片本身就够了。
sitemap.ts 和 robots.ts 要跟页面策略一致
App Router 可以直接在 app/ 下生成 sitemap 和 robots。它们不是装饰文件,应该和实际索引策略一致:重要页面放进 sitemap,不想被抓取的后台、搜索结果页、重复参数页用 robots 或 noindex 控制。
tsx// app/sitemap.ts import type { MetadataRoute } from 'next'; export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const posts = await getPosts(); const baseUrl = 'https://example.com'; return [ { url: baseUrl, lastModified: new Date(), changeFrequency: 'daily', priority: 1 }, ...posts.map((post) => ({ url: `${baseUrl}/blog/${post.slug}`, lastModified: new Date(post.updatedAt), changeFrequency: 'weekly' as const, priority: 0.8, })), ]; }
tsx// 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', }; }
如果某些页面已经 canonical 到主页面,通常就不该再把大量重复 URL 塞进 sitemap。
Core Web Vitals 会影响真实搜索表现
SEO 不只看内容,也看体验。Core Web Vitals 里最常见的问题是 LCP 慢、CLS 抖动、INP 差。
- LCP:首屏主图太大、接口太慢、字体阻塞都会拖慢。用
next/image、服务端渲染、CDN 和合理缓存处理。 - CLS:图片没写宽高、广告位突然插入、字体切换都会造成布局偏移。
- INP:客户端 JavaScript 太重、长任务太多,会让交互变钝。能放服务器组件的逻辑别搬到客户端。
Pages Router 可以用 reportWebVitals 上报指标;App Router 项目也可以放一个客户端组件专门采集指标,再发送到分析服务。
tsx'use client'; import { useReportWebVitals } from 'next/web-vitals'; export function WebVitalsReporter() { useReportWebVitals((metric) => { navigator.sendBeacon('/analytics/web-vitals', JSON.stringify(metric)); }); return null; }
数据要按页面类型看。首页、文章页、商品页的问题往往不一样,把所有页面混在一起看平均值,很容易误判。
多语言页面要处理 hreflang、canonical 和路径
国际化站点最容易出问题的是重复内容。中文、英文、西语页面如果只是 URL 不同,metadata 却全一样,搜索引擎很难判断该给哪个地区用户展示。
tsxexport async function generateMetadata({ params }: { params: { locale: string } }) { const t = await getTranslations(params.locale); return { title: t.title, description: t.description, alternates: { canonical: `https://example.com/${params.locale}`, languages: { 'zh-CN': 'https://example.com/zh-CN', en: 'https://example.com/en', es: 'https://example.com/es', }, }, }; }
hreflang 的核心是互相指向:中文页声明英文页,英文页也要声明中文页。canonical 则指向当前语言的主 URL,不要所有语言都 canonical 到英文页。
OG 和 Twitter 决定分享时像不像一个正常页面
社交分享本身不是传统排名因素,但会影响点击和传播。文章页至少准备标题、描述、封面图、类型和 URL。封面图建议使用 1200×630,标题不要截断,描述不要和全站默认文案一模一样。
App Router 的 metadata 已经能覆盖大多数 OG/Twitter 场景。如果页面有特殊分享图,可以用 opengraph-image.tsx 动态生成,但要注意生成耗时和缓存。
一份更贴近项目的检查顺序
做 Next.js SEO 时,可以按这个顺序排查:
- 页面是否能在禁用 JavaScript 后看到核心内容。
- 标题、描述、canonical、OG/Twitter 是否按页面动态生成。
- App Router 是否使用 Metadata API 和
generateMetadata,旧 Pages Router 是否正确使用Head。 - 详情页是否有 JSON-LD,且数据和页面可见内容一致。
- 重要动态路由是否能通过内部链接和 sitemap 被发现。
- 渲染策略是否合理:静态内容用 SSG/ISR,强实时内容才用 SSR。
- 图片是否使用
next/image,首屏图是否优化 LCP,尺寸是否避免 CLS。 sitemap.ts、robots.ts、i18n hreflang、canonical 是否互相一致。- Web Vitals 是否按页面类型监控,而不是只看全站平均值。
Next.js 已经把很多 SEO 基础能力放进框架里,但框架不会替你判断页面值不值得索引,也不会替你写出有用内容。真正有效的做法,是让 App Router 的元数据、预渲染、结构化数据、语义化 HTML 和性能优化服务于同一个目标:用户打开页面后,看到的正是搜索结果承诺的内容。