Appearance
网站的 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 → 重新渲染 → LCP8 步串行,LCP 轻松超过 4 秒。优化路径:
- 关键 CSS 内联到
<head>——浏览器不需要等外部 CSS 就能开始渲染 - LCP 图片用
<link rel="preload">——不要在 JS 里通过 state 动态赋值 src fetchpriority="high"——确保 LCP 图片在所有图片中优先加载- 服务端预取数据——写入
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 异常检测系统。
收录三阶段
- Discovery:URL 提交到 Google(sitemap、内链、或手动)。GSC 的 URL 检查工具可以强制触发发现
- Crawl:Googlebot 访问 URL。"抓取统计信息"显示每天的抓取请求数、响应时间、HTTP 状态码分布
- 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 Performance | 32 |
| FCP | 3.6s |
| LCP | 5.8s |
| CLS | 0.18 |
| TTFB | 180ms |
| 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:数据对比
| 指标 | Before | After | 变化 |
|---|---|---|---|
| Lighthouse Performance | 32 | 98 | +206% |
| Lighthouse SEO | 64 | 100 | +56% |
| FCP | 3.6s | 0.8s | -78% |
| LCP | 5.8s | 1.2s | -79% |
| CLS | 0.18 | 0.02 | -89% |
| TTFB | 180ms | 45ms | -75% |
| Google 已索引页面 | 2 / 52 | 52 / 52 | 全覆盖 |
| 日均自然搜索点击 | < 5 | 180+ | 36x |
| GSC 搜索展现 | ~200 | ~8,000 | 40x |
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 标签配置。
