陌上人如玉
公子世无双

9.7 SEO 与元数据

9.7 SEO 与元数据

一句话:SEO 的四根支柱是「可抓取的内容(SSR/SSG)、准确的元数据(title/description/OG)、结构化数据(JSON-LD)、以及「Core Web Vitals 性能」;Next.js 的 metadata API 让元数据可以「声明式 + 动态」生成。

一、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 };
}

八、面试延伸

  1. 「前端怎么做 SEO?」
    四件事:① 让内容可被抓取——用 SSR / SSG / ISR 输出「带真实内容的 HTML」(纯 CSR 的「空 HTML」爬虫读不到内容);② 准确的元数据——title(最重要)、description、Open Graph(分享卡片)、canonical(避免重复内容);③ 结构化数据(JSON-LD)——让搜索引擎理解内容语义(文章、商品、问答等,可产生富摘要);④ 性能——Core Web Vitals(LCP/INP/CLS)既是体验指标也是排名因素。

  2. 「为什么分享卡片必须用 SSR?」
    因为社交平台的爬虫(微信、飞书、Twitter、Facebook、Slack)通常不执行 JavaScript——它们只读取「服务端返回的 HTML 里的 meta 标签」。如果 og:title、og:image 是客户端 JS 动态插入的,爬虫看到的是「空的 HTML」→ 卡片就没有标题/图片。所以「分享卡片相关元数据必须在服务端渲染的 HTML 里」(Next.js 的 generateMetadata 就是为此设计的)。

  3. 「generateMetadata 和 metadata 有什么区别?」
    ① export const metadata——静态元数据对象(在模块加载时确定),适合「不依赖路由参数/数据」的页面;② export async function generateMetadata({ params, searchParams })——动态生成(可以是 async,能根据路由参数查数据),适合「详情页」(title/description/OG 图都依赖具体内容)。两者不能同时导出(会报错),并且都只在 Server Component 里可用。

  4. 「OG 图为什么必须是绝对 URL?」
    因为分享平台/爬虫是在「自己的服务器上」抓取并渲染卡片的——它们拿到的是「一个 URL 字符串」,然后去请求这张图。如果 og:image 是相对路径(/og.png),爬虫无法知道「相对于哪个域名」(它并不知道你的页面部署在哪)。所以必须是完整的绝对 URL(https://example.com/og.png)。Next.js 里通过 metadataBase 让「相对路径被解析成绝对 URL」(避免在每个页面手写域名)。

  5. 「结构化数据(JSON-LD)有什么用?」
    它让搜索引擎理解内容的语义——告诉它「这是一个文章(BlogPosting,含作者/发布时间)」「这是一个商品(Product,含价格/库存)」「这是一组问答(FAQPage)」。有了它,搜索结果可以显示「富摘要」(评分星级、价格、FAQ 折叠、面包屑),从而提高点击率。注意:① 内容必须与页面实际一致(虚标会被惩罚);② 用 JSON.stringify 生成(避免转义错误);③ 要放在「服务端渲染的 HTML」里(CSR 注入的读不到)。

  6. 「canonical 是干什么的?」
    canonical(规范链接)告诉搜索引擎「哪个 URL 是这个内容的「正版」」。它解决的问题是「同一内容有多个 URL」导致的权重分散:① 带查询参数的地址(?page=1、?utm_source=xxx);② 多个域名(www 与不带 www、http 与 https);③ 分页/排序/筛选产生的变体。注意:分页中的「第 2 页」的 canonical 应该指向自己(不要都指向第 1 页,否则爬虫可能不去抓第 2 页内容)。

  7. 「Core Web Vitals 和 SEO 有什么关系?」
    它们是「排名因素之一」(Google 已确认页面体验纳入排名),同时也是「用户体验指标」:① LCP(最大内容绘制)——主要内容多快出现(目标 < 2.5s);② INP(交互到下次绘制)——交互响应多快(目标 < 200ms);③ CLS(累积布局偏移)——布局是否稳定(目标 < 0.1)。优化手段与「性能优化」完全一致(SSR/SSG 输出关键内容、图片优化、减少长任务、给图片与骨架设定宽高避免 CLS)。

  8. 「为什么不要用 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 路由做内容页(# 之后不发给服务器)。

赞(0) 打赏
未经允许不得转载:陌上寒 » 9.7 SEO 与元数据

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫