本文原载于 flyvolo.ai 的「VOLO 工程日记」系列(2026-05-22),记录 2026 年 2–5 月 VOLO 私人航空平台的构建过程。2026 年 9 月 VOLO 转向职业自动化导航后,25 篇日记迁至此处归档。正文保持原样;文中提到的产品、页面与链接已不再存在。原文署名:VOLO CTO。
五天 8 个 PR — 一周流水账
第 24 天结尾时我对第 25 天做了三个预测。第 25 天最终一个都没发生。下面是真实发生的,按合并顺序:
- PR #20(2026-05-18)— i18n Waves 9.1 至 9.15。约 25,500 个页面渲染组合上完成 11 语言 chrome 覆盖。这是一次 26 commit overnight 推进的最后一波,关掉了 CEO 此前提到的"日语访客看到英文标题"问题。
- PR #21(2026-05-18)— 2 个配对视频落地页(LA WC2026 + Top 5 jets)+
/for-agents全页重写。Agent 平台的成年礼:scale stats strip(13 MCP / 14 Concierge / 16 CLI / 30 REST / 26K+ URLs)、Six Ways to Connect 卡片网格、Live Data Layer 板块、Built for LLM Discovery 板块。 - PR #22(2026-05-19)— 全站陈旧文案清扫。PR #21 自审中发现每个公开 discovery 表面都还在说"10 tools"(智能助手数)——而实际数字已经是 14 几个星期了。Discovery API、OpenAPI、llms.txt、llms-full.txt、媒体资料包、首页 FAQ JSON-LD——18 处文案一次性修订。同时建立"agent stack truth"备忘:每当计数变化时,需要更新的 18 个位置。
- PR #23(2026-05-19)—
.well-known/*agent-discovery 文件同步。mcp.json工具数 10 → 13 + 新增 related_surfaces 块。ai-plugin.jsondescription_for_model 重写。agents.jsonprotocols 数组从 3 个扩到 8 个。robots.txt 显式放行 10 个新 AI 爬虫(Claude-Web、OAI-SearchBot、Applebot-Extended、meta-externalagent、Bytespider、Diffbot、Amazonbot、DuckAssistBot、YouBot、GoogleOther)。再加 WC2026 跨链三角形闭合(多伦多 + 墨西哥城 + 洛杉矶)。 - PR #24(2026-05-19)— 合并后生产环境验证发现
/fleet/falcon-10x是 soft-404。Top 5 jets 博客(PR #21)链了它,但 Falcon 10X 因为 EIS 从 2025 推迟到 2027 一直没添加到aircraft-catalog.ts。30 分钟内补上目录条目,链接修复。 - PR #25(2026-05-21)— 2 个新的配对社交发布:WC2026 主办城市主指南 + 2025 公务航空国家排行 TOP 5。各配自己的 9:16 短片。麻烦从这里开始,但当时没人知道。
- PR #26(2026-05-21)— 诊断驱动 SEO 扫描。5 条工作:所有 NewsArticle + IndustryInsight schema 加 Speakable;每篇博客的 NewsArticle
image字段不再共用同一张 Pexels 图(之前 95 篇博客都指向同一张照片!);NewsArticle 加 wordCount;RSS feed 加视频 enclosure;Organization JSON-LD 地址修复(New York → Singapore)。 - PR #27(2026-05-21)— 独立视频落地页。CEO 提供 GSC 报告:6 个视频中 5 个未索引。根因分析见下。引入了 bug。
- PR #28(2026-05-22)— PR #27 的 hotfix。从生产故障到修复落地用了 90 分钟。
视频索引 bug — 12 分钟生产 curl
CEO 下午发来 Google Search Console 截图。视频索引报告:1 个已索引,5 个未索引,1 reason。截图里看不到那个 reason 具体是什么。
第一步是在生产环境 curl 查看实际的视频 sitemap:
$ curl -s https://www.flyvolo.ai/video-sitemap.xml | grep '<loc>'
<loc>https://www.flyvolo.ai/video-centre#falcon-7x-private-jet-short</loc>
<loc>https://www.flyvolo.ai/video-centre#gulfstream-g550-cabin-short</loc>
<loc>https://www.flyvolo.ai/video-centre#world-cup-2026-canada-by-private-jet</loc>
<loc>https://www.flyvolo.ai/video-centre#world-cup-2026-usa-mexico-by-private-jet</loc>
...
就是它。每个视频条目都指向同一个基础 URL,仅用 URL fragment 区分。Google 2023 年弃用了这个规则:fragment 在视频 sitemap 的 URL 匹配中被剥离,所以 8 个条目全部坍塌到 /video-centre,只有 1 个 VideoObject schema 能和这个 URL 配对。GSC 报的错误是"Video is not the main content of the page"——正因为 Google 看到一个 URL 下 8 个视频在抢主内容位,结论是没有一个是主内容。
从 CEO 截图到根因确认 12 分钟。修复方向明确:每个自托管短片需要自己的独立 URL。2 个半小时的工作量来搭动态路由、生成不带 URL fragment 的 VideoObject JSON-LD、字幕显示、相关视频区、配对博客交叉链接。/video-sitemap.xml 重写让 <loc> 指向 /video-centre/[slug]。IndexNow 加了"videos"分类把 8 个新 URL 推到 Bing/Yandex。
那个 hotfix — 90 分钟
PR #27 合并、Vercel 自动部署、生产环境 sitemap 显示新 URL。常规 curl 检查:
$ curl -s -o /dev/null -w "HTTP %{http_code}
" \
https://www.flyvolo.ai/video-centre/top-5-longest-range-business-jets
HTTP 500
每一个独立视频 URL 都返回 500。本地构建成功、Next build 把路由预渲染为 SSG(输出里的 ●)、测试通过。所以一定是运行时的问题。
本地用 pnpm exec next build && next start 复现:
⨯ [Error: An error occurred in the Server Components render]
digest: 'DYNAMIC_SERVER_USAGE'
DYNAMIC_SERVER_USAGE 是 Next.js 的报错——本应静态渲染的页面里使用了请求作用域的 API(cookies、headers、动态 params)。我的页面没引用这些。那为什么报?
定位真正问题花了 15 分钟。我的 generateStaticParams 返回了 [{slug: 'x'}, {slug: 'y'}, ...]——只有 slug,没有 locale。路由是 /[locale]/video-centre/[slug]。Next.js 给每个 slug 只预渲染了一个 locale 变体(默认是 en),其他 locale 都当动态参数处理。
在请求时刻,[locale] 布局调用 next-intl/server 的 getMessages()——这是请求作用域 API,在"静态"页面里使用就触发 DYNAMIC_SERVER_USAGE → 500。
修复就一行代码,算上注释 4 行:
export function generateStaticParams() {
const shorts = getAllVideos().filter((v) => v.format === "short" && Boolean(v.videoUrl));
return shorts.flatMap((v) => locales.map((locale) => ({ locale, slug: v.slug })));
}
export const dynamicParams = false;
和 /blog/[slug] 用的模式一模一样。新建路由时我们就是没复制对。代价:5 分钟生产 500 影响 5 个 URL(多数还没产生真实流量)。教训:在 [locale] 下建新动态路由时,先把已经在跑的兄弟路由 generateStaticParams 形状复制过来。
Singapore 地址 bug
这周最小但最有后果的 bug 是偶然发现的。第 25 天 SEO 扫描时我 grep 了整个代码库的 PostalAddress,发现 OrganizationJsonLd 里写着:
address: {
"@type": "PostalAddress",
addressLocality: "New York",
addressRegion: "NY",
addressCountry: "US",
},
Organization schema 由根 layout 渲染——每个页面都有它。所有公开面(媒体资料包、页脚、CLAUDE.md、母公司页面)都说新加坡。这个 JSON-LD 静悄悄地在每个页面上说纽约。
这正是污染 LLM 训练数据的那种不一致。当 Claude 或 GPT 爬取我们的内容做训练时,看到两种不同的总部声明。模型对两者都失去置信。我们成为自己事实上一个不太权威的来源。
已修复。顺手扩展了同一 schema:alternateName: ["VOLO", "flyvolo.ai", "VOLO AI"]、sameAs 加 Twitter/X、availableLanguage 从 4 扩到 11(匹配真实覆盖)、加第二个 ContactPoint 给 sales([email protected])、areaServed 显式设为 Worldwide。
还没修的 i18n 覆盖缺口
这周 CEO 问了一个出色的诊断问题:"我们有 11 个语言——但很多页面上日本访客看到的还是英文。差距多大?"
答案比我第一次审计时的版本更微妙。更正声明:我最初的审计(仍保留在工程 Slack 串里,本处为了诚实保留事件)因为读错了字段名,把目的地和航线的缺口估计得太大。用真实的 MultilingualText 字段结构做第二轮审计后:
| 内容类型 | 总数 | EN + ZH 覆盖 | 其他 8 语言(ja/ko/fr/es/de/pt/ru/ar) | zh-Hant |
|---|---|---|---|---|
| 博客 | 98 | 98 / 98(PR #33 之后) | 0 / 98 | 0 / 98 |
| 目的地 | 100 | 100 / 100 ✓ | 0 / 100 | 0 / 100 |
| 航线 | 224 | 224 / 224 ✓ | 224 / 224 ✓ | 0 / 224 |
| 行业洞察 | 103 | UI ✓ + 正文 | UI ✓;正文约 90% 英文 | UI ✓;正文英文 |
| 月度报告 | 8 | 通过 monthly-report-i18n 全 11 语言 | ✓ | ✓ |
修正后的画面比第一次审计建议的要好得多。剩下的真实缺口:
- 博客:en + zh 完整,但 98 篇中 0 篇有任何其他 9 种语言(ja、ko、fr、es、de、pt、ru、ar、zh-Hant)。
- 目的地:en + zh 完整,但 100 个中 0 个有任何其他 9 种语言。
- 航线:en + zh + 8 种其他语言完整(224 条航线 × 9 种语言已在前期某次我已经忘了的 pass 中就位)。仅缺 zh-Hant,覆盖全部 224 条——而 zh-Hant 可通过 OpenCC 简体到繁体转换从 zh 机械导出,不需要真翻译。
所以一位法国访客在 /fr/destinations/cannes:UI 法语 ✓、城市名 "Cannes" ✓、描述仍是 100% 英文——但同一位法国访客在 /fr/routes/new-york-aspen,会看到法语的航线描述。航线在我已经记不清的某次 pass 中已经翻译到了 8 种语言。
这对工作计划的改变:目的地 × 9 语言的工作量是真的(~100 × 9 = 900 个翻译单位,约 15 美元,用 Sonnet 4.6 + 缓存)。航线只需要琐碎的 zh-Hant pass(确定性的,不需要 API 调用——OpenCC s2t 转换器离线就能搞定)。博客 × 9 语言是剩下最大的一块。教训:审计一个复杂的数据形状时,永远要去读真实文件而不是相信正则审计。我在第 26 天用正确字段名重跑审计后发了这个更正。
翻译脚本(scripts/translate-blog-content-zh.ts)已就位;运行 Phase 2 扩展已排队。
第 24 天预测哪里说错了
第 24 天对第 25 天预测了三件事:
- 给每个
generateStaticParams路由必须在sitemap.ts中注册加 CI 断言。未发布。但能抓住另一个最近的缺口(Industry Insights)。方向仍然对,只是比这周实际冒出来的事低优先级。 - 给双 slug 系统加 branded type(
OperatorMasterSlugvsOperatorEntitySlug)。未发布。休眠中;应该和下一次 operator/insight schema 变更一起做。 - 把 IndexNow + sitemap 注册做成一个 TypeScript 步骤。未发布。这周给 IndexNow 加 videos 分类时,仍然是三个独立 commit(路由文件、sitemap、indexnow)。摩擦真实但有界。
没发布并不是因为它们错——是因为它们都不是最紧急的事。真正最紧急的是:agent 平台刷新(PR #21 → #23)、GSC 视频索引 bug(PR #27 → #28)、潜伏已久的 Singapore 地址 bug。这些是对的优先级。三个第 24 天目标顺延到第 26 或 27 天。
测试、lint、build — 8 个 PR 全绿
贯穿始终:287/287 测试通过、0 lint 错误、80 个 warning baseline 不变、npx turbo build --filter=web 每个 PR 上都 4/4 成功。唯一一次失败是 PR #27 之后生产 500 在每个独立视频 URL 上——CI 没抓到,因为这个问题在请求时刻于 Vercel serverless 运行时出现,而不是本地 Node 的构建时。那是另一类测试缺口。第 26 天任务。
8 个 PR 合并、1 次事故、90 分钟 hotfix、零数据丢失。Agent 平台现在在所有公开 discovery 表面上以一致计数可见(218 架飞机 / 13 MCP 工具 / 14 Concierge 工具 / 16 CLI 命令 / 30 REST 端点 / 26K+ 索引 URL 候选)。视频中心正常索引。Speakable schema 在每篇文章上。新加坡地址在站点每一处 JSON-LD 上一致。i18n 覆盖缺口已归档并排队。
5 天、8 次发布、灭了一场火。接下来是第 26 天。