工程日記・第十四天:欺騙 Google 的 Sitemap、安全審計、以及 3,680 個 URL 終於可見
Google Search Console 顯示 87% 的頁面未被索引:267 個已索引,2,028 個不可見,23 個 Merchant Listing 錯誤。我們一天內提交了 5 個 commit——安全加固(分佈式限流、CSP、JWT)、SEO schema 修復,以及一次 sitemap 重寫(順便發現了 Next.js 16 的空 XML bug)。最終結果:3,680 個 URL 的有效 sitemap,零 schema 錯誤,800+ 條新內部交叉鏈接。
87% 的隱形問題
一切始於 CEO 發來的 Google Search Console 截圖。數據觸目驚心:
| 指標 | 數值 |
|---|---|
| 總點擊數 | 13 |
| 已索引頁面 | 267 |
| 未索引 | 2,028 |
| 無效 Merchant Listing | 23 |
| Core Web Vitals | 無數據 |
87% 的頁面對 Google 不可見。我們的 sitemap 有 3,400+ 個 URL,但 Google 只索引了 267 個。另有 23 個頁面有無效的 Merchant Listing 結構化數據。今天的任務:全部修復——外加一次遲到的安全審計。
第一階段:安全加固
在動 SEO 之前,我們先處理了基礎設施審計中發現的四個安全問題:
分佈式限流
我們的限流器是內存版的——一個 Map<string, number[]>。在 Vercel 的無服務器架構上,每次函數調用都有獨立的內存空間。攻擊者只需請求不同實例就能繞過限流。我們用 Vercel KV(Upstash Redis) 的原生 REST API 重寫了它——零新 npm 依賴:
| 方面 | 之前 | 之後 |
|---|---|---|
| 存儲 | 內存 Map | Vercel KV (Redis) + 內存回退 |
| API | 同步 | 異步 (await) |
| 一致性 | 僅單實例 | 全局跨實例 |
| 依賴 | 無 | 無(原生 fetch 到 Upstash REST) |
| 更新的 API 路由 | — | 9 個文件 |
CSP 加固
從內容安全策略中移除了 unsafe-eval,爲 Google Analytics 4 添加了明確的域名白名單。這能阻止依賴 eval() 的 XSS 攻擊,同時保持分析功能正常。
JWT 刷新令牌密鑰
刷新令牌之前用的是訪問令牌的密鑰加一個簡單字符串拼接。如果訪問密鑰泄露,刷新令牌也會淪陷。現在使用獨立的 JWT_REFRESH_SECRET 環境變量。
密碼策略
爲密碼驗證新增了 OWASP 標準的特殊字符要求。現在強制執行五項驗證規則:長度、大寫、小寫、數字、特殊字符。
第二階段:SEO Schema 手術
23 個無效 Merchant Listing 的根本原因很清楚:我們把物流退貨政策應用到了服務業務上。私人包機是服務,不是你裝箱發快遞的實物商品。Google 的 Merchant Listing 驗證器拒絕了所有含 hasMerchantReturnPolicy 和 shippingDetails 的頁面。
修復很精準——從 3 個文件中刪除所有物流/退貨 schema:
JsonLd.tsx— 徹底刪除MERCHANT_RETURN_POLICY和SHIPPING_DETAILS常量fleet/[slug]/page.tsx— 移除內聯物流/退貨 schemaCatalogDetailPage.tsx— 同樣處理
同一提交中的其他 schema 修復:
- 爲
SAMPLE_REVIEW添加了datePublished(Google 必需) - 移除了航線價格頁面
AggregateOffer中無效的嵌套offers[] - 移除了乘客屬性中無效的
unitCode: "C62" - 通過
buildAlternates()爲全部 3,680 個 URL 添加了x-defaulthreflang
第三階段:Sitemap 危機
有趣的部分來了。深度審計顯示技術上一切正確——所有 13 個動態路由都有 generateStaticParams,sitemap 覆蓋全面,robots.txt 正確,沒有 noindex 標籤。那 Google 爲什麼忽略 87% 的頁面?
三個根本原因:
1. lastModified 在撒謊
我們 sitemap 中的 3,340 個條目全部使用 new Date() 作爲 lastModified。每次 Google 爬取 sitemap,我們都在說:"所有 3,340 個頁面剛剛被修改。" Google 的爬蟲預算有限。當所有東西都聲稱是最新的,就沒有任何東西能獲得優先級。我們用穩定的部署日期替換了 new Date(),博客文章用實際的 post.date,行業報告用 report.publishDate。
2. generateSitemaps() 的 Bug
我們嘗試用 Next.js 16 的 generateSitemaps() API 將 sitemap 拆分爲 7 個分類塊。想法是好的——讓 Google 處理更小的、專注的 sitemap,機隊(908 URL)、機場+FBO(1,024 URL)等,而不是一個巨大的文件。
構建成功了。構建輸出顯示了 7 個 sitemap 文件。測試通過了。但每個 .xml.body 文件都恰好是 110 字節——一個空的 <urlset/>,零個 <url> 條目。
問題在哪?Next.js 16.1.6 在 SSG 預渲染期間調用 sitemap 函數,但 generateSitemaps() 對所有塊都生成了空 XML。我們的單元測試通過了,因爲它們直接調用 sitemap({ id: 0 })。構建的 SSG 管道做了……別的什麼事。我們花了兩個 commit 調試這個問題(包括一次 Number(id) 類型強制轉換的嘗試),最終接受了務實的解決方案:回退到單文件 sitemap。
一個文件 3,680 個 URL(2.2MB)遠低於 Google 的 50,000 URL 上限。拆分是優化,不是必需。
3. IndexNow 在低聲說話
我們的 IndexNow 端點只提交了 14 個核心頁面。IndexNow 每個請求支持最多 10,000 個 URL。我們將其擴展爲自動收集 11 個類別的所有內容 URL(機隊、航線、目的地、機場、FBO、運營商、空飛、博客、案例、報告、靜態頁),並支持批量處理。
第四階段:內部交叉鏈接
Google 通過鏈接發現頁面,不僅僅是 sitemap。審計發現機隊詳情頁有一個"推薦航線"版塊,顯示類似 New York (TEB) → Miami (OPF) 的城市對——但它們是純文本,沒有鏈接。200+ 架飛機頁面 × 4 條航線 = 800+ 條被浪費的內部鏈接。
我們將 IdealRoutes 從純展示的 <div> 轉換爲可點擊的 <Link> 卡片,指向 /contact?from=城市A&to=城市B&aircraft=機型,並新增了"瀏覽所有航線"鏈接到 /routes。這創造了 800+ 條新內部鏈接,將 PageRank 從機隊頁面分配到航線目錄。
數據總結
| 指標 | 之前 | 之後 |
|---|---|---|
| Sitemap URL 數 | 3,340(部署時爲空) | 3,680(已驗證有效 XML) |
| 無效 Merchant Listing | 23 | 0 |
| hreflang x-default | 缺失 | 全部 3,680 個 URL |
| lastModified 準確性 | 全部"今天" | 穩定部署日期 + 真實日期 |
| IndexNow 覆蓋 | 14 個頁面 | ~900 個內容 URL |
| 新增內部交叉鏈接 | 0 | 800+ |
| 限流器 | 內存版(無服務器上失效) | Vercel KV + 回退 |
| CSP unsafe-eval | 存在 | 已移除 |
| 更新的 API 路由 | — | 9 |
| Commit 數 | — | 5 |
| 變更文件 | — | 19 |
提交記錄
5cda031 — 安全:分佈式限流、CSP 加固、JWT 刷新密鑰、密碼策略。
26bad98 — SEO:修復 Merchant Listing 錯誤,添加 hreflang x-default,清理結構化數據。
77015ea — SEO:拆分 sitemap index,修復 lastModified,擴展 IndexNow,添加交叉鏈接。
0836387 → 7c4408d — 修復:發現 Next.js 16 空 XML bug 後回退 generateSitemaps()。
最可怕的 bug 不是拋出錯誤的那種——而是默默成功的那種。我們的 sitemap 構建成功,通過了所有測試,部署無警告,並向每個爬蟲提供了有效的 XML。只不過這些 XML 恰好包含零個 URL。Google 忠實地讀取了我們的空 sitemap,並精確地索引了我們告訴它的內容:什麼都沒有。兩個小時調試一個綠色構建,才發現 Next.js 16 的 generateSitemaps() 在 SSG 期間生成空的 urlset。教訓:永遠 curl 你自己的生產 URL。
準備好飛行了嗎?幾秒鐘獲取個性化包機報價。
訂閱資訊
空腿航班優惠、新航線與航空洞察,直達您的郵箱。