返回博客

Next.js App Router 技术 SEO 实用清单

Next.js App Router 技术 SEO 实用清单

2026年10月9日 — SEO

发布于

2026年10月9日

阅读时长

4 分钟

分类

SEO

标签

  • SEO
  • Next.js
  • App Router
  • Metadata
  • Structured Data

(摘要)在 Next.js App Router 中配置 Metadata、canonical、sitemap、robots.txt、结构化数据、hreflang 和 Core Web Vitals,附可直接复制的代码。

Next.js 提供了做技术 SEO 所需的一切,但几乎没有一项是默认开启的。这是我在每个 App Router 项目上都会过一遍的清单,大致按影响大小排序。

1. 重要内容用服务端渲染

搜索引擎能执行 JavaScript,但对服务端渲染的 HTML 收录得更快、更稳定。在 App Router 中,服务端组件是默认选项——把标题、正文和链接放在服务端组件里,客户端组件只负责交互,不负责内容。

快速检测:禁用 JavaScript 打开页面,或者运行 curl https://yoursite.com/page。如果响应里没有主要文字,第一轮收录里也不会有。

2. 设置 metadataBase 和标题模板

在根布局中:

app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  title: {
    default: 'Example Studio — Web Design and Development',
    template: '%s — Example Studio',
  },
  description: 'Websites and products for small teams: design, development and launch.',
  openGraph: { siteName: 'Example Studio', type: 'website' },
}

metadataBase 会把相对 URL(canonical、Open Graph 图片)转成绝对 URL。有了标题模板,子页面只需设置自己那部分标题。

3. 每个页面都有独立的标题和描述

每个需要被收录的页面都要有自己的 <title>(约 50–60 个字符,关键词靠前)和 meta description(约 150–160 个字符,写得让人想点击)。动态页面使用 generateMetadata:

app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) return {}
  return {
    title: post.title,
    description: post.excerpt,
    alternates: { canonical: `/blog/${post.slug}` },
    openGraph: {
      type: 'article',
      publishedTime: post.date,
      images: [{ url: post.cover, width: 1200, height: 630 }],
    },
  }
}

4. Canonical 链接

每个页面都设置 alternates.canonical。它能防止查询参数(?utm_source=...)、末尾斜杠和备用域名造成的重复内容。选定一种形式——带不带 www、带不带末尾斜杠——然后在托管平台把其他形式重定向过来。

5. Sitemap

app/sitemap.ts 会生成 /sitemap.xml,静态导出也支持:

app/sitemap.ts
import type { MetadataRoute } from 'next'
import { getAllPosts } from '@/lib/blog'

export const dynamic = 'force-static'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const base = 'https://example.com'
  const posts = await getAllPosts()
  return [
    { url: base, changeFrequency: 'monthly', priority: 1 },
    ...posts.map((post) => ({
      url: `${base}/blog/${post.slug}`,
      lastModified: post.date,
      changeFrequency: 'yearly' as const,
      priority: 0.7,
    })),
  ]
}

在 Google Search Console 和 Bing 网站管理员工具中各提交一次。如果主要面向国内用户,也别忘了百度搜索资源平台。

6. robots.txt

app/robots.ts
import type { MetadataRoute } from 'next'

export const dynamic = 'force-static'

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

不要用 robots.txt 来阻止页面被收录——被屏蔽的页面仍可能通过外链被收录。应该在页面的 metadata 中使用 robots: { index: false }。

7. 结构化数据(JSON-LD)

结构化数据帮助搜索引擎理解页面是什么。对于文章,在页面中渲染一个 script 标签:

tsx
const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  headline: post.title,
  description: post.excerpt,
  datePublished: post.date,
  image: `https://example.com${post.cover}`,
  author: { '@type': 'Person', name: 'Jane Doe', url: 'https://example.com' },
}

return (
  <>
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') }}
    />
    <article>{/* ... */}</article>
  </>
)

对小型企业有用的类型:首页用 Organization 或 Person,文章用 BlogPosting,深层页面用 BreadcrumbList,商品页用 Product。用 Google 的富媒体搜索结果测试工具验证。

8. Open Graph 图片

在领英、X、Slack 和各种聊天软件里分享链接时,会用到 Open Graph 标签。给每个页面一张 1200×630 像素的图片。可以在页面旁边放一个静态的 opengraph-image.png,也可以在 opengraph-image.tsx 中用 ImageResponse 生成(静态导出时在构建阶段生成,所以不能依赖请求信息)。

9. 多语言网站:真实的 URL 和 hreflang

每种语言都需要自己的 URL,否则搜索引擎永远只能看到其中一种。用 localStorage 在客户端切换语言对访客没问题,但 Google 看不到。使用语言路由段——app/[lang]/...——并声明各语言版本:

ts
alternates: {
  canonical: '/en/blog/my-post',
  languages: {
    en: '/en/blog/my-post',
    'zh-CN': '/zh/blog/my-post',
    'sr-Latn': '/sr/blog/my-post',
    'x-default': '/en/blog/my-post',
  },
}

每个语言版本都应列出所有其他版本,包括它自己。

10. 内部链接和标题层级

  • 每页一个 <h1>,然后按顺序使用 <h2>/<h3>。标题是大纲,不是字号。
  • 相关页面之间互相链接,并使用描述性的锚文本("静态导出指南",而不是"点击这里")。使用 <Link>,确保它们是真正的 <a href> 元素。
  • 重要页面离首页不超过三次点击。

11. Core Web Vitals

速度是排名信号,更重要的是转化信号。用 PageSpeed Insights 测量(实地数据来自真实的 Chrome 用户),目标是 LCP ≤ 2.5 秒、INP ≤ 200 毫秒、CLS ≤ 0.1。App Router 项目中常见的问题:首屏图片过大、客户端 JavaScript 过多,以及字体或未设尺寸的图片造成的布局偏移。如果你的网站动画很多,可以看动画网站的 Core Web Vitals 优化。

12. 图片

  • 内容图片写描述性的 alt,装饰性图片用空的 alt=""。
  • 明确的 width/height 或 aspect-ratio,防止布局偏移。
  • 使用现代格式(AVIF、WebP)和合适的尺寸。
  • 描述性的文件名:nextjs-sitemap-example.png,而不是 IMG_2041.png。

13. 404 和重定向

对不存在的页面返回真正的 404(App Router 中用 notFound())。移动或重命名页面时,从旧 URL 加一个 301 重定向——在静态导出中,就是托管平台的 _redirects 规则。

精简版清单

  • 内容放在服务端组件中
  • 根布局中设置 metadataBase 和标题模板
  • 每个页面都有独立的标题、描述和 canonical
  • sitemap.ts 和 robots.ts,并提交 sitemap
  • 文章、组织、面包屑的 JSON-LD
  • 每个页面都有 Open Graph 图片
  • 每种语言一个 URL,并配置 hreflang
  • 合理的标题层级和描述性内部链接
  • Core Web Vitals 全部达标
  • 真正的 404,以及移动页面的 301

在做静态网站?阅读 Next.js 静态导出完全指南,了解没有服务器时哪些地方不一样。

订阅

通过邮件接收新文章

有新文章时发一封简短的邮件。不发垃圾邮件,随时可以退订。

不用 CMS 和数据库,在 Next.js 中搭建 Markdown 博客
不用 CMS 和数据库,在 Next.js 中搭建 Markdown 博客
下一篇 →