Skip to content

网站的 SEO 深度解析

搜索引擎的工作原理

前端侧对 SEO 的讨论经常停留在 <meta> 标签层面,但这只是很小的一部分。即使 meta 标签完整,搜索引擎也可能根本看不到页面主体内容。

SEO 的根基是搜索引擎的三层架构:Crawler → Indexer → Ranker。三层之间有严格的时序依赖,爬虫没发现页面就进不了索引,索引阶段被降权排名就不可能好。任何一层出问题,后面都是无效投入。

抓取层

搜索引擎蜘蛛从已知 URL 出发,沿 <a> 链接扩散发现新页面。这里有两个容易被低估的约束。

Crawl Budget。Google 给每个网站分配的抓取配额从每天几百到几万不等,取决于你的 PageRank 和更新频率。一个百万页面的电商站,每个商品加 5 个筛选参数就能生成数千个变体 URL——爬虫可能永远触达不到真正的核心产品页。

更致命的是 robots.txt 误配。开发环境用 Disallow: / 阻止所有爬虫,上线时忘记改成 Allow——这种事故我见过不止一次。Google 发现 robots.txt 屏蔽后 24 小时内停止抓取,但恢复后重新收录需要数周。

还有一个细节:CSS 和 JS 文件不能被 robots.txt 阻止。Google 需要渲染页面来判断移动友好性,屏蔽了 /static/ 目录,Google 看到的就是无样式纯文本——排名信号直接受损。

性能直接决定抓取效率。TTFB 每增加 100ms,爬虫每分钟能抓取的页面数下降约 10-20%。这不是线性关系——爬虫有连接数上限和超时预算,慢的页面不只是排名差,它可能根本没机会被完整抓取。

索引层的两阶段模型

Google 的索引机制是这个领域最关键的概念,分两个阶段:

第一阶段 — 解析原始 HTML。爬虫拿到 HTTP 响应后立即提取文本、标题、链接、结构化数据。不执行 JavaScript,秒级完成。

第二阶段 — 渲染 JavaScript。资源允许时,Google Web Rendering Service(WRS)用 headless Chrome 打开页面,执行 JS,等 DOM 稳定后再次提取内容。有几小时到几天的延迟,且约 5-10 秒的渲染超时。

如果页面是纯 CSR,HTML 中只有 <div id="app"></div>,那么 Google 在第一阶段对页面内容的了解基本为零。即使第二阶段渲染成功,中间的索引空白期仍然会影响检索可见性。

更隐蔽的问题:WRS 渲染超时后只有已渲染的内容被索引。一个 2MB 的 JS bundle 加 3 个串行 API 请求,渲染完成需要 8 秒——大概率在超时前只渲染了一半,Google 索引到的是不完整的页面。

这就是为什么 SSR/SSG 对 SEO 不是"更好"——是"必须"。内容必须在第一阶段就出现在 HTML 中

Bing 和百度对 JS 渲染的支持也明显弱于 Google。如果目标用户主要来自这些搜索引擎生态,纯 CSR 的 SEO 覆盖面会受到更强约束。


选渲染策略,不是选框架

同一个 Next.js,可以配成 CSR(SEO 灾难)、SSR(SEO 友好)、SSG(SEO 最优)、ISR(SEO 友好 + 内容新鲜度)。框架只是工具,渲染模型才是决策。

决策矩阵

text
             HTML 内容时机    首次索引速度    内容新鲜度    运维成本
CSR          JS 执行后        天级延迟        实时          低
SSR          每次请求时        秒级            实时          高(Node 服务器)
SSG          构建时            秒级            构建频率      极低(CDN)
ISR          首次请求后缓存    首次秒级        按 revalidate  中
Dynamic      两套渲染管道      秒级(蜘蛛路由) 实时          高(维护两套)

形式化决策树

text
你的页面内容需要实时更新吗?
├─ 是 → 内容是否依赖用户状态/登录?
│       ├─ 是 → SSR(Next.js / Nuxt 3)
│       └─ 否 → ISR(Next.js + revalidate / Nuxt + swr)
└─ 否 → 网站类型是什么?
        ├─ Markdown 内容站 →  VitePress / VuePress(SSG)
        ├─ 电商 / CMS 内容 →  Next.js ISR / Nuxt 3 SSG+ISR
        └─ SPA 老项目改造 →  
            ├─ 资源充足 →  迁移到 SSR 框架
            └─ 资源紧张 →  Dynamic Rendering(过渡)→ 计划性迁移

CSR 对搜索引擎是"内容黑洞"。SSR 理论上是动态内容的最优 SEO 方案——每次请求返回完整 HTML。代价也明确:Node.js 服务器的运维成本、高并发下的 TTFB 压力、每个请求的内存开销。

SSG 是内容站的全局最优解——构建时生成完整 HTML,部署到 CDN,TTFB 50ms 以内。1000 页的 SSG 站点构建时间从 30 秒到 10 分钟不等。代价是每次内容更新都要重新构建。

ISR 的问题在 Cold Cache——revalidate 设 60 秒而网站流量低,Googlebot 访问时正好赶上 cache 过期,触发一次等同于 SSR 的慢请求。对爬虫来说这次访问的表现决定了整批页面的索引优先级。

Dynamic Rendering 为什么只能是过渡方案

根据 User-Agent 分流——蜘蛛看到预渲染的静态 HTML,用户看到 SPA。Google 明确说这是过渡方案。

核心风险是 Cloaking:蜘蛛看到的 HTML 和用户看到的 HTML 内容差异过大,Google 判定为伪装内容直接降权。维护两套渲染管道也意味着每次 UI 变更要同步两个管道——工程成本最终会超过一次性的 SSR 迁移。


四个框架的 SEO 实现

VitePress:内容站的最优解

VitePress 的 SEO 能力来自它的架构本质——纯编译时 SSR。构建时每个 .md 文件通过 renderPage() 生成完整 HTML,<title><meta><link> 全部在构建时注入。输出到 dist/ 的是搜索引擎可以直接完整索引的静态文件。

HTML 在 CDN 上,TTFB 30-80ms,Googlebot 拿到 HTML 瞬间提取 100% 页面内容。不需要 WRS 渲染,索引速度是所有方案中最快的。

局限是元数据必须在 frontmatter 或 config 中预定义,无法根据用户状态动态生成。对内容驱动的网站这几乎无影响。

VuePress 2:插件系统的代价

VuePress 2 多了插件系统,@vuepress/plugin-seo 可以自动从正文截取 180 字符作为 description、自动生成 canonical URL。但插件链的执行顺序带来复杂性——每个插件都可能修改 head/meta/content,测试 SEO 标签输出比 VitePress 困难。

选型逻辑很简单:纯内容站选 VitePress(更简单更快),需要插件生态选 VuePress 2(更灵活但更复杂)。

Next.js App Router:Metadata 是行业标杆

Next.js 的 Metadata 层级合并机制值得深入理解。Layout 的 metadata 和 page 的 metadata 是深度合并,不是替换。generateMetadata() 可以异步从数据库获取数据——SEO 标签可以是动态的,但不影响 HTML 渲染的完整性。

Sitemap 被抽象为数据函数。sitemap.ts 导出的是一个生成函数,generateSitemaps() 对百万级 URL 自动分片。这种设计反映了现代 SEO 架构的核心思维:URL 列表应该从数据源派生,不是手动维护的静态文件。

但 App Router 的 Streaming SSR 引入了一个新变量——页面内容以流式传输,Google 会等待 </body></html> 标签到达。如果流式渲染中有组件卡住(比如慢 SQL),整个 HTML 交付被延迟,影响索引。

Nuxt 3:Edge 部署被低估了

Nuxt 3 的 Nitro 引擎预置了多部署目标预设(Vercel、Cloudflare Workers、Netlify、Node.js),SSR 服务可以部署到 Edge。TTFB 从传统 Node 服务器的 200-400ms 降到 Edge 的 50-100ms。

useServerSeoMeta() 只在服务端执行,SEO 标签不暴露在客户端 JS bundle 中,避免了 hydration 时的标签闪烁。

Nuxt 3 SEO 模块生态(sitemap、og-image、robots)在快速扩张,但成熟度落后于 Next.js Metadata API。需要精细化 SEO 标签管理的项目,Next.js 仍是更强的选择。


性能如何影响 SEO:不只是 Lighthouse 分数

Google 把 Core Web Vitals 作为排名信号,但这套信号如何作用到排名链路里,经常会被简化理解。

Page Experience 信号的结构:

text
Page Experience 评分
  ├─ Core Web Vitals
  │   ├─ LCP ≤ 2500ms
  │   ├─ INP ≤ 200ms
  │   └─ CLS ≤ 0.1
  ├─ 移动友好性
  ├─ HTTPS
  └─ 无侵入性插页广告

CWV 影响的是排名倾向性——两个内容质量相当的页面,CWV 达标的排在前面。但如果内容质量远不如竞品,CWV 全绿也救不了排名。

性能预算:分三层防线

把性能当作工程约束而非优化目标,需要建立分层防线:

text
Layer 1: 构建时检测
  └─ Lighthouse CI assertions (budget.json)
     ├─ LCP > 2500ms → CI failed
     ├─ CLS > 0.1 → CI failed
     └─ bundle size > 200KB (gzip) → warning

Layer 2: 发布前检测
  └─ PR preview URL → PageSpeed Insights API
     └─ 和 main 分支基线对比,退化 > 10% → block

Layer 3: 生产监控
  └─ CrUX API + RUM (web-vitals.js)
     ├─ P75 LCP 周环比 > 15% → P2 告警
     └─ P95 LCP > 5000ms → P1 告警

LCP 的瓶颈不在图片本身

在典型的 SPA 中,LCP 元素的发现链路是这样:

text
HTML → CSS 下载 → CSSOM 构建 → JS 下载 → JS 解析
→ React 渲染 → API 请求 → 更新 state → 重新渲染 → LCP

8 步串行,LCP 轻松超过 4 秒。优化路径:

  1. 关键 CSS 内联到 <head>——浏览器不需要等外部 CSS 就能开始渲染
  2. LCP 图片用 <link rel="preload">——不要在 JS 里通过 state 动态赋值 src
  3. fetchpriority="high"——确保 LCP 图片在所有图片中优先加载
  4. 服务端预取数据——写入 window.__DATA__={...},省掉客户端一轮网络请求。这对 WRS 渲染尤其关键——数据已经在 HTML 里,WRS 不需要额外等待 API 请求

CLS 最难排查的是字体

图片没有 width/height 导致的 CLS 最容易修复——加上 aspect-ratio 或显式宽高。

字体切换引起的 CLS 更难排查。font-display: optional 最彻底——字体 100ms 内没加载完就用 fallback,之后也不切换。代价是用户看不到自定义字体。如果品牌字体至关重要,用 font-display: swap 配合 size-adjust 调整 fallback 字体的 ascent/descent 使其接近 Web Font,从根本上减少切换偏移。

异步广告和第三方嵌入也是 CLS 大户——一个 300px 的广告 iframe 异步插入,下面所有内容瞬间下移。给广告位用 min-height 或 CSS Grid 预留空间,即使广告没加载布局也不塌陷。

TTFB 与 Crawl Budget 的量化关系

TTFB 是一串延迟的总和:

text
Redirect → Service Worker → DNS → TCP → TLS → Server Processing → First Byte

静态站点 CDN 部署的 TTFB 可以到 30-80ms。Next.js/Nuxt SSR 未优化通常 200-600ms,数据库查询慢的可以超过 1.5s。

这里对 SEO 的影响是直接的:TTFB 每增加 100ms,爬虫每分钟能抓取的页面数下降 10-20%。一个 TTFB 600ms 的网站,爬虫每分钟能抓取约 40 个页面;TTFB 降到 80ms,这个数字可以到 80-100。性能优化的 SEO 收益 = 更多页面被更快收录。


Google Search Console:把它当信号系统用

GSC 常被当成后台面板看索引数量。实际上它应该被当作 SEO 异常检测系统。

收录三阶段

  1. Discovery:URL 提交到 Google(sitemap、内链、或手动)。GSC 的 URL 检查工具可以强制触发发现
  2. Crawl:Googlebot 访问 URL。"抓取统计信息"显示每天的抓取请求数、响应时间、HTTP 状态码分布
  3. Index:内容通过质量评估进入索引库。"页面索引"报告显示三种状态

索引异常排查决策树

"已发现但未收录"数字异常增长时,按顺序排查:

text
1. robots.txt 是否屏蔽了关键资源?
   └─ 用 GSC robots.txt 测试工具逐条验证

2. HTTP 状态码是否正确?
   └─ "抓取统计信息"中查看状态码分布(200? 301? 5xx?)

3. Canonical URL 是否正确?
   └─ Google 选择的 canonical 可能不是你想的那个

4. 内容质量是否达到索引门槛?
   └─ "已抓取但未收录"数量异常 → 内容质量问题

5. 结构化数据是否有错误?
   └─ "增强功能"报告中查看错误详情

6. JS 渲染是否超时?
   └─ GSC "查看已测试的页面" → 截图,对比禁用 JS 时的页面

GSC API 自动化监控

javascript
const { google } = require('googleapis');
const searchConsole = google.webmasters('v3');

async function getSearchAnalytics(siteUrl, startDate, endDate) {
  const res = await searchConsole.searchanalytics.query({
    siteUrl,
    requestBody: {
      startDate, endDate,
      dimensions: ['page', 'query'],
      rowLimit: 500,
    },
  });
  return res.data.rows;
}

线上部署:每日定时拉取搜索分析 → 和 7 天滑动窗口基线比较 → 点击量周环比下跌 > 20% → 自动告警。


案例:VitePress 技术博客 SEO 改造

Before:CSR SPA 的 SEO 死局

指标
Lighthouse Performance32
FCP3.6s
LCP5.8s
CLS0.18
TTFB180ms
Google 已索引页面2 / 52
日均自然搜索点击< 5
GSC 搜索展现~200 / 天

HTML 响应体是 <div id="app"></div> + 3 个 JS bundle(总计 480KB 未压缩)。GSC URL 检查工具截图是空页面——不带 JS 渲染时完全无内容。

方案选择

方案SEO 收益工程成本运营成本决策
Next.js SSR❌ 过度设计
Nuxt 3 SSG❌ 不需要动态功能
VitePress极低✅ 最优

选择 VitePress 的逻辑:所有文章都是 .md 文件,不需要动态数据、用户状态、个性化。SSR/ISR 的复杂性是负收益。

实施关键步骤

SEO 元数据配置

typescript
// .vitepress/config.ts
export default defineConfig({
  head: [
    ['link', { rel: 'icon', href: '/favicon.ico' }],
    ['meta', { name: 'author', content: '作者名' }],
    ['meta', { property: 'og:type', content: 'article' }],
    ['meta', { name: 'twitter:card', content: 'summary_large_image' }],
  ],
  transformHead({ pageData }) {
    const head = [];
    if (pageData.title) {
      head.push(['meta', { property: 'og:title', content: pageData.title }]);
    }
    if (pageData.description) {
      head.push(['meta', { property: 'og:description', content: pageData.description }]);
    }
    return head;
  },
});

结构化数据注入

typescript
transformPageData(pageData) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'TechArticle',
    headline: pageData.title,
    datePublished: pageData.frontmatter.date,
    author: { '@type': 'Person', name: '作者名' },
    description: pageData.frontmatter.description,
  };
  pageData.frontmatter.head = pageData.frontmatter.head || [];
  pageData.frontmatter.head.push([
    'script',
    { type: 'application/ld+json' },
    JSON.stringify(schema),
  ]);
}

Lighthouse CI 性能预算

javascript
// .lighthouserc.js
module.exports = {
  ci: {
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'categories:seo': ['error', { minScore: 0.95 }],
        'largest-contentful-paint': ['error', { maxLength: 2500 }],
        'cumulative-layout-shift': ['error', { maxLength: 0.1 }],
      },
    },
  },
};

After:数据对比

指标BeforeAfter变化
Lighthouse Performance3298+206%
Lighthouse SEO64100+56%
FCP3.6s0.8s-78%
LCP5.8s1.2s-79%
CLS0.180.02-89%
TTFB180ms45ms-75%
Google 已索引页面2 / 5252 / 52全覆盖
日均自然搜索点击< 5180+36x
GSC 搜索展现~200~8,00040x

GSC 收录时间线:

  • Day 0:提交 sitemap
  • Day 2-3:Google 开始抓取,52 个 URL 全部被发现
  • Day 5-7:约 20 页进入索引
  • Day 14:42/52 已索引,搜索展现显著上升
  • Day 30:全部索引,日均展现 ~5,000,日均点击 120+
  • Day 60:日均展现 ~8,000,日均点击 180+,稳定增长

踩过的坑

description 为空导致搜索结果展示导航文本。10 篇早期文章没设 description,Google 搜索结果展示的是导航栏文字。修复:补上 description + transformHead() 里加 fallback——从正文前 180 字符截取。

代码块花括号破坏 HTML。VitePress 构建时不转义 markdown 代码块中的花括号。某篇文章的代码包含 { title: "SEO" },构建出的 HTML 花括号被解析为实体。修在 markdown.preConfig 中配转义选项。

URL 结构变更 = 权重丢失。从 /blog/xxx 改为 /posts/xxx 后,GSC 视为全新 URL,旧排名权重完全丢失。URL 结构一旦确定不要轻易改。必须改时配 301 重定向。

Lighthouse CI 分数 3-5 分的波动是正常的。CI runner 的 CPU 和网络差异导致抖动。minScore: 0.90 而不是 minScore: 0.98——只拦截显著退化。


国际化 SEO

有国际化需求的项目,需要考虑 hreflang 标签和多区域部署架构。

hreflang 告诉搜索引擎"这个页面在另一个语言下有对应版本":

html
<link rel="alternate" hreflang="en" href="https://example.com/en/seo-guide">
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh/seo-guide">
<link rel="alternate" hreflang="x-default" href="https://example.com/seo-guide">

三件事需要注意:每个语言版本必须双向引用(en 指向 zh,zh 也要指向 en);hreflang 和 canonical 不要冲突(canonical 应该指向当前语言版本的 URL);多语言 sitemap 可以单独提交也可以在一个 sitemap 中用 <xhtml:link> 标注。

多区域部署的架构选择:

  • example.com/en/(子目录)——SEO 权重集中在一个域名
  • en.example.com(子域名)——Google 可能视为独立站点,分散权重
  • example.co.uk(ccTLD)——地域信号最强,但管理成本最高

还需要注意搜索引擎差异。百度在国内占主导但几乎不执行 JS——面向中国市场的 SEO 必须 SSR/SSG。Naver 在韩国主导,有自己的站长工具(Naver Webmaster Tools),需要单独提交 sitemap。


把 SEO 当作系统而非配置

把 SEO 仅仅视为 <meta> 标签配置问题,本身就是一种过度简化。

从工程视角看,SEO 是一个需要持续维护的系统:

  • 渲染策略决定搜索引擎能否看到内容(SSR/SSG/ISR 的选择)
  • 性能预算决定排名中的页面体验信号(三层防线:构建时 → 发布前 → 生产监控)
  • 监控体系决定索引异常能否及时发现(GSC API + 滑动窗口告警)
  • 结构化数据决定搜索结果中的展现形式(Rich Results + JSON-LD)

没有银弹——VitePress 对内容站是 SEO 最优解,但需要用户状态和动态数据时 Next.js SSR 才是正解。理解搜索引擎的工作原理、掌握渲染策略的 tradeoff、建立性能与索引的监控闭环——这才是 SEO 的工程化路径,不是一次性的 meta 标签配置。