← 全部文章← All writing

VOLO 工程日记

工程日记・第十一天:Google 说不 —— 1,700 页 SEO 紧急修复

本文原载于 flyvolo.ai 的「VOLO 工程日记」系列(2026-02-26),记录 2026 年 2–5 月 VOLO 私人航空平台的构建过程。2026 年 9 月 VOLO 转向职业自动化导航后,25 篇日记迁至此处归档。正文保持原样;文中提到的产品、页面与链接已不再存在。原文署名:VOLO CTO。

痛苦仪表盘

当你上线一个拥有 3,400+ URL 的多语言网站,一周后打开 Google Search Console,你应该做好坏消息的心理准备。我们没有。

六个错误类别盯着我们,每个都是"Google 无法或不愿索引你的页面"的不同变体:

  • 服务器错误 (5xx) — 100+ URL 渲染崩溃
  • 含正确 canonical 标签的备用页面 — 118 个页面 canonical 引用错误
  • 无用户选定 canonical 的重复页面 — 2 个页面
  • 未找到 (404) — 1 个遗留 URL
  • 已发现 - 尚未索引 — 1,720 个页面 Google 发现但未访问
  • 已抓取 - 当前未索引 — 61 个页面 Google 访问后明确拒绝

每个类别有不同的根因。每个需要不同的修复。这是在一个 session 中诊断并解决全部六个问题的故事。

Bug 1:5xx 大屠杀

超过 100 个 URL 返回服务器错误。模式立即显现:所有动态页面的非英文 locale 变体都在崩溃。/fr/operators/netjets — 500。/zh/airports/teterboro — 500。/es/case-studies/diplomatic-charter — 500。英文版本正常。

根因:六个页面文件中的 generateStaticParams() 只返回 { slug },没有包含 { locale, slug }。在 Next.js App Router 中,当页面位于 [locale]/[slug] 下时,静态参数必须包含两个动态段。没有 locale,Next.js 只预渲染默认 locale 变体。其他 locale 在运行时命中服务器,由于这些是静态数据页面没有服务端回退,直接崩溃。

修复是机械性的,但必须精确应用于六个文件——运营商、空腿航班、机场、机场 FBO、单个 FBO 详情和案例研究。六个文件变更,约 1,065 个 URL 从 5xx 恢复到 200。本次 session 中影响最大的单次修复。

Bug 2:Canonical 的背叛

118 个页面被标记为"含正确 canonical 标签的备用页面"。这意味着 Google 发现了页面,看到其 canonical URL 指向其他地方,决定索引 canonical 目标。我们的法语、中文和西班牙语页面全部将 canonical 指向英文版本。

问题出在共享 SEO 工具函数 buildAlternates()。它总是生成英文裸 URL 作为 canonical,不管哪个 locale 在渲染。正确行为:每个 locale 变体应该自引用。/fr/fleet/citation-x 的 canonical 应该是 /fr/fleet/citation-x,而不是 /fleet/citation-x。

但修复工具函数只是一半的战斗。每个调用 buildAlternates() 的页面都需要传入 locale 参数。更糟的是:34 个页面使用 export const metadata——一个无法访问路由参数的静态导出。每一个都必须转换为能从 params 提取 locale 的动态 generateMetadata() 函数。53 个文件在一次提交中变更。

Bug 3:幽灵 URL

一个 404:/quote。我们几周前把页面重命名为 /contact,但从没设置重定向。next.config.ts 中一行代码:301 永久重定向,告诉 Google 将所有排名信号从旧 URL 转移到新 URL。

Bug 4:1,720 个未发现的页面

"已发现 - 尚未索引"意味着 Google 找到了这些 URL(在我们的 sitemap 或内部链接中)但还没有派爬虫访问。两个行动:首先,通过 IndexNow 协议提交全部 3,428 个 URL,同时 ping Bing、Yandex、Seznam 和 Naver。其次,分析内部链接结构,发现关键 hub 页面——/airports、/operators、/case-studies——只在 footer 中有链接,header 导航中缺失。

Header 是网站上最重要的内部链接元素。每个页面都渲染 header,意味着其中的每个链接都获得最大的抓取优先级。我们重构了 header 导航:Destinations 变成下拉菜单,包含 Destinations hub、Airports、Operators 和 Case Studies。About 下拉新增 For Agents 链接。四个语言的翻译文件全部更新。

Bug 5:内容贫乏的拒绝

"已抓取 - 当前未索引"是 Search Console 中最令人担忧的状态。意味着 Google 实际访问了页面、评估了内容,然后决定不值得索引。这是主动拒绝。

61 个页面被拒绝。模式:主要是机队 catalog 页面及其 locale 变体。诊断:我们的机队有两层数据。15 架飞机有丰富详细的内容——400+ 字的双语描述。但 184 架飞机是仅有 catalog 数据的条目。这些页面使用共享的 CatalogDetailPage 组件,为同一类别的所有飞机显示相同的类别级描述。40 多架轻型喷气机共享相同段落。Google 正确地将其识别为内容贫乏的重复内容。

修复:构建 generateAircraftIntro() 函数,根据每架飞机的具体规格——制造商、航程分类、客舱尺寸、巡航速度等级、WiFi 可用性和定价——创建独特的介绍段落。184 个 catalog 页面现在各自以独特段落开头,后接类别级内容。

Bug 6:构建时间回退

修复所有 locale 变体的 generateStaticParams 后,引入了新问题:Vercel 构建时间从 2 分钟飙升到 12 分钟。数学很简单——从预渲染约 199 个页面变成 796 个(199 机型 × 4 locales)。

解决方案:只预渲染需要预渲染的。15 个详细机型页面有丰富内容值得静态生成。184 个 catalog 页面更轻,可以按需渲染。generateStaticParams 从返回 796 页改为 60 页(15 × 4 locales)。构建时间恢复到合理范围。

今天发布的内容

提交修复文件变更影响页面
7f6b740为 6 种页面类型添加 locale 到 generateStaticParams6~1,065 URL 从 5xx 恢复
df49944所有 locale 变体的自引用 canonical URL53118+ 页面 canonical 修正
9737f12301 重定向 /quote → /contact11 个遗留 URL 保留
a70b825Header 导航添加 airports/operators/case-studies5所有页面(改善内部链接)
f39b583184 个 catalog 页面的独特机型描述1736 页面(184 × 4 locales)
2528dc2仅在构建时预渲染 15 个详细机型1构建时间:12min → ~5min
合计67 文件~3,400 URL 受影响

另外:通过 IndexNow 提交 3,428 个 URL 以加速重新抓取。

经验教训

国际化不只是翻译。添加 next-intl 和翻译字符串只是第一步。第二步是确保 SEO 基础设施的每一个环节——静态参数、canonical URL、metadata 函数——都是 locale 感知的。我们有翻译,但忘了教框架。

静态 metadata 是陷阱。在 Next.js App Router 中,export const metadata 看起来简洁。但页面一旦在 [locale] 等动态段下,你就需要运行时访问路由参数,即需要 generateMetadata()。我们一次性转换了 34 个页面。教训:多语言站点从第一天起就用动态 metadata。

内容贫乏是真实信号。Google 拒绝 catalog 页面不是因为技术错误,而是因为内容确实贫乏——同一类别 40+ 页面共享相同段落。修复不是 meta 标签或配置变更,而是写更好的内容。

构建时间是功能。从 2 分钟部署变成 12 分钟不只是不便——它改变了你的发布方式。快速迭代依赖快速反馈循环。我们选择减少预渲染、按需缓存,用 catalog 页面首次访问的延迟换取 60% 的构建时间缩减。

SEO 不是你发布一次的功能。它是与网站野心成正比地断裂的基础设施。10 页的营销网站不需要这些。3,400 页的多语言平台,动态路由、四个 locale、199 架飞机?关于静态生成、canonical URL 和内容唯一性的每一个假设,都在规模面前被压力测试。今天我们通过了测试——但只因为 Google 告诉我们正在失败。