工程師與貓
ESC
Content
    ↑↓ navigate open esc close
    Published on

    Next.js 時區陷阱:當 K8s 環境讓你的日期判斷晚了 8 小時

    Authors
    • avatar
      Name
      Alex Yu

    前陣子收到 SEO 團隊回報,說網站有些文章明明已經發布了,但在 Google Search Console 上卻顯示 robots: noindex,導致文章無法被 Google 索引。

    這問題有點詭異,因為同樣的邏輯在另一個專案運作正常,偏偏這個網站就出包。追查之後發現,這是一個時區處理的經典陷阱。

    問題現象

    以文章 ID 360131 為例:

    • API 回傳的 publish_time2026-01-05 16:33:00(台北時間)
    • Googlebot 爬取時間2026-01-05 22:50:33(台北時間)
    • 預期結果:文章已發布 6 小時,應該顯示 index, follow
    • 實際結果:顯示 noindex, nofollow

    文章明明已經發布超過 6 小時,為什麼系統還判斷它是「未來稿」?

    根本原因:伺服器時區不是你想的那樣

    問題出在這段判斷「未來稿」的程式碼:

    // ❌ 有問題的程式碼
    import { parse, isAfter } from "date-fns";
     
    export function isFutureDraft(publishTime: string): boolean {
      const now = new Date();
      const publishTime_UTC8 = parse(publishTime, "yyyy-MM-dd HH:mm:ss", new Date());
      return isAfter(publishTime_UTC8, now);
    }

    這段程式碼看起來沒問題,但有一個隱藏的假設:伺服器的本地時區是 UTC+8(台北時間)

    K8s 環境的時區

    問題在於,我們的應用部署在 K8s(Kubernetes) 環境,而 K8s Pod 預設的時區是 UTC,不是 UTC+8。

    date-fnsparse 函數解析時間字串時,它會把結果當作本地時區的時間:

    // 在 K8s (UTC 時區) 環境下執行
    const publishTime = "2026-01-05 16:33:00";
    const parsed = parse(publishTime, "yyyy-MM-dd HH:mm:ss", new Date());
     
    console.log(parsed.toISOString());
    // 輸出:2026-01-05T16:33:00.000Z  ← 被當成 UTC 時間!
    // 實際應該是:2026-01-05T08:33:00.000Z(台北 16:33 = UTC 08:33)

    錯誤的時間差

    這導致時間判斷晚了 8 小時

    時間點正確值錯誤值
    publish_time(UTC)08:33:0016:33:00
    Googlebot 爬取時間(UTC)14:50:3314:50:33
    判斷結果08:33 < 14:50 → 已發布16:33 > 14:50 → 未來稿

    因為錯誤地把台北時間當成 UTC 解析,導致判斷時多了 8 小時的偏差。

    為什麼另一個專案沒問題?

    同事之前在另一個專案就有處理過這個情況,使用了不同的寫法:

    // ✅ 另一個專案的寫法(正確)
    const publishTime = new Date(
      `${article.data.publish_time.replace(' ', 'T')}+08:00`
    );

    這種寫法明確指定了 +08:00 時區偏移量,所以不管伺服器是什麼時區,解析結果都是正確的。

    解決方案:使用 date-fns-tz

    修復方式是使用 date-fns-tz 套件,明確處理時區轉換:

    // ✅ 修復後的程式碼
    import { isAfter } from "date-fns";
    import { fromZonedTime } from "date-fns-tz";
     
    export function isFutureDraft(publishTime: string): boolean {
      try {
        const now = new Date();
     
        // publishTime 是台北時區的時間,需要明確轉換
        const TAIPEI_TIMEZONE = "Asia/Taipei";
        const publishTimeInTaipei = fromZonedTime(publishTime, TAIPEI_TIMEZONE);
     
        return isAfter(publishTimeInTaipei, now);
      } catch {
        return false; // 解析失敗時,預設為非未來稿
      }
    }

    fromZonedTime 的作用

    fromZonedTime 的功能是:把一個「某時區的本地時間字串」轉換成正確的 UTC 時間

    import { fromZonedTime } from "date-fns-tz";
     
    const taipeiTimeString = "2026-01-05 16:33:00";
    const result = fromZonedTime(taipeiTimeString, "Asia/Taipei");
     
    console.log(result.toISOString());
    // 輸出:2026-01-05T08:33:00.000Z  ← 正確的 UTC 時間

    這樣不管伺服器是 UTC、UTC+8 還是其他時區,轉換結果都會是正確的。

    補上單元測試

    為了確保這個 bug 不會再發生,我補上了模擬 K8s 環境的測試:

    import { isFutureDraft } from "../index";
     
    describe("isFutureDraft", () => {
      beforeEach(() => {
        // 模擬 K8s UTC 環境:2026-01-05 14:50:33 UTC
        jest.useFakeTimers();
        jest.setSystemTime(new Date("2026-01-05T14:50:33.000Z"));
      });
     
      afterEach(() => {
        jest.useRealTimers();
      });
     
      it("should correctly identify published article in UTC environment", () => {
        // 台北 16:33 = UTC 08:33,在 UTC 14:50 時已經發布
        const publishTime = "2026-01-05 16:33:00";
     
        expect(isFutureDraft(publishTime)).toBe(false); // ← 應該是已發布
      });
     
      it("should correctly identify future article", () => {
        // 台北 23:00 = UTC 15:00,在 UTC 14:50 時還沒到發布時間
        const publishTime = "2026-01-05 23:00:00";
     
        expect(isFutureDraft(publishTime)).toBe(true); // ← 應該是未來稿
      });
    });

    重點整理

    1. 不要假設伺服器時區:在容器化環境(Docker、K8s)中,時區通常是 UTC,不是你開發機的時區
    2. 明確處理時區:使用 date-fns-tz 或在時間字串中加上時區資訊(如 +08:00
    3. date-fns parse 的陷阱parse 函數會將結果當作本地時區,這在不同環境可能產生不同結果
    4. 寫測試模擬生產環境:用 jest.setSystemTime 模擬 UTC 環境,確保時區邏輯正確

    個人心得

    這個 bug 讓我體會到,時區問題真的是「開發機上跑得好好的,部署後就爆炸」的經典代表。

    以前總覺得「時區問題很簡單啊,就加個偏移量」,但實際上要考慮的細節很多:伺服器時區、資料庫時區、API 回傳格式、前端顯示…每個環節都可能出錯。

    這次的教訓是:永遠明確指定時區,不要依賴隱式的本地時區轉換

    希望這篇文章能幫助到遇到類似問題的開發者。

    參考資料