切换语言
切换主题

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.tsrobots.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 定义了几百种类型,但对大部分网站来说,常用的就这几种:

  1. Organization - 公司/组织信息(放在首页)
  2. BlogPosting - 博客文章(每篇文章都加)
  3. Product - 产品信息(电商必备,包含价格、评分、库存)
  4. FAQPage - 常见问题(FAQ 页面,能在搜索结果中直接展开)
  5. 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。配置一次,后面都是复制粘贴。

验证你的结构化数据

配完之后怎么知道对不对?用这两个工具:

  1. Google Rich Results Testhttps://search.google.com/test/rich-results)

    • 输入你的页面 URL,Google 会告诉你能展示哪些富媒体
    • 如果有错误,会明确指出哪里不对
  2. Schema Markup Validatorhttps://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 部分。

测试和验证社交分享效果

配完之后,别急着发,先用这些工具测试:

  1. Facebook Sharing Debuggerhttps://developers.facebook.com/tools/debug/)

    • 输入 URL,看 Facebook 会怎么抓取和显示
    • 注意:Facebook 会缓存 OG 信息,如果改了代码,需要在这个工具里点”Scrape Again”刷新缓存
  2. Twitter Card Validatorhttps://cards-dev.twitter.com/validator)

    • 输入 URL,预览 Twitter 卡片效果
    • 注:2023 年后这个工具需要 Twitter 开发者账号才能用,但可以直接在 Twitter 发推测试
  3. LinkedIn Post Inspectorhttps://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 生成这个文件。

配置后记得做这两件事

  1. 提交到 Google Search Consolehttps://search.google.com/search-console)
  2. 提交到 Bing Webmaster Toolshttps://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)也没必要抓
  • 草稿、预览页面可以用 noindex meta 标签或 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 图片搜索是一个巨大的流量入口。

两个关键点

  1. 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 指标之一)
  1. 必须写 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',
  }
}

部署后的验证清单

配完之后,上线前一定要检查:

  1. 查看 HTML 源码

    • 在浏览器按 F12 → Elements → 查看 <head> 标签
    • 确认 <title><meta name="description">、OG 标签都正确渲染
  2. 测试 sitemap 和 robots

    • 访问 https://yourdomain.com/sitemap.xml,看是否正常生成
    • 访问 https://yourdomain.com/robots.txt,检查内容
  3. 验证结构化数据

    • 用 Google Rich Results Test 测试几个页面
    • 确认没有错误或警告
  4. 测试社交分享

    • 用 Facebook Sharing Debugger 和 Twitter Card Validator 测试
    • 确认图片、标题、描述都正确显示
  5. 提交到搜索引擎

    • 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

    步骤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

    步骤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

    步骤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

    步骤4: 配置sitemap和robots.txt

    创建sitemap.ts:
    • 导出default函数返回sitemap数组
    • 包含所有页面的URL、lastModified、changeFrequency
    • 支持动态生成

    创建robots.ts:
    • 配置允许/禁止的爬虫
    • 设置sitemap路径
    • 配置爬取规则
  5. 5

    步骤5: 优化图片SEO

    图片优化要点:
    • 使用next/image组件
    • 添加有意义的alt文本
    • 配置图片尺寸(1200x630适合OG图片)
    • 使用WebP/AVIF格式
    • 添加图片结构化数据(ImageObject)
  6. 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 有什么关系?
SSR(服务端渲染)只是让HTML在服务端生成,但SEO还需要正确配置meta标签、结构化数据、Open Graph等。SSR不等于SEO友好,必须主动配置Metadata API才能让搜索引擎正确理解和索引内容。
Metadata API 和 Head 组件有什么区别?
Metadata API是Next.js 15推荐的方式,类型安全、支持静态和动态metadata、自动处理重复标签。Head组件是React的方式,需要手动管理,容易出错。新项目建议使用Metadata API。
如何为动态路由配置metadata?
使用generateMetadata函数,接收params参数,可以async获取数据,然后返回metadata对象。

例如:
export async function generateMetadata({ params }) {
const data = await getData(params.id)
return { title: data.title }
}
Open Graph 图片尺寸应该多大?
推荐尺寸是1200x630像素,这是Facebook、Twitter、LinkedIn等平台都支持的万能尺寸。图片文件大小建议控制在1MB以内,格式使用JPEG或PNG。
结构化数据是必须的吗?
不是必须的,但强烈推荐。结构化数据能让搜索结果显示富媒体(评分、价格、FAQ等),提升点击率。Google、Bing等搜索引擎都支持JSON-LD格式的结构化数据。
sitemap 和 robots.txt 需要手动创建吗?
不需要。Next.js支持创建sitemap.ts和robots.ts文件,自动生成sitemap.xml和robots.txt。sitemap.ts可以动态生成所有页面的URL,robots.ts可以配置爬取规则。
SEO 优化后多久能看到效果?
通常需要1-3个月。搜索引擎需要时间抓取和索引新内容。

建议:
1) 提交sitemap到Google Search Console
2) 使用Google Rich Results Test验证
3) 持续监控Search Console数据
4) 保持内容更新

17 分钟阅读 · 发布于: 2025年12月19日 · 修改于: 2026年1月22日

评论

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

相关文章