工程日记・第十一天:Google 说不 —— 1,700 页 SEO 紧急修复
痛苦仪表盘
当你上线一个拥有 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 到 generateStaticParams | 6 | ~1,065 URL 从 5xx 恢复 |
df49944 | 所有 locale 变体的自引用 canonical URL | 53 | 118+ 页面 canonical 修正 |
9737f12 | 301 重定向 /quote → /contact | 1 | 1 个遗留 URL 保留 |
a70b825 | Header 导航添加 airports/operators/case-studies | 5 | 所有页面(改善内部链接) |
f39b583 | 184 个 catalog 页面的独特机型描述 | 1 | 736 页面(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 告诉我们正在失败。
订阅资讯
空腿航班优惠、新航线与航空洞察,直达您的邮箱。