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

- Name
- Alex Yu
前陣子收到 SEO 團隊回報,說網站有些文章明明已經發布了,但在 Google Search Console 上卻顯示 robots: noindex,導致文章無法被 Google 索引。
這問題有點詭異,因為同樣的邏輯在另一個專案運作正常,偏偏這個網站就出包。追查之後發現,這是一個時區處理的經典陷阱。
問題現象
以文章 ID 360131 為例:
- API 回傳的 publish_time:
2026-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-fns 的 parse 函數解析時間字串時,它會把結果當作本地時區的時間:
// 在 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:00 | 16:33:00 |
| Googlebot 爬取時間(UTC) | 14:50:33 | 14: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); // ← 應該是未來稿
});
});重點整理
- 不要假設伺服器時區:在容器化環境(Docker、K8s)中,時區通常是 UTC,不是你開發機的時區
- 明確處理時區:使用
date-fns-tz或在時間字串中加上時區資訊(如+08:00) - date-fns parse 的陷阱:
parse函數會將結果當作本地時區,這在不同環境可能產生不同結果 - 寫測試模擬生產環境:用
jest.setSystemTime模擬 UTC 環境,確保時區邏輯正確
個人心得
這個 bug 讓我體會到,時區問題真的是「開發機上跑得好好的,部署後就爆炸」的經典代表。
以前總覺得「時區問題很簡單啊,就加個偏移量」,但實際上要考慮的細節很多:伺服器時區、資料庫時區、API 回傳格式、前端顯示...每個環節都可能出錯。
這次的教訓是:永遠明確指定時區,不要依賴隱式的本地時區轉換。
希望這篇文章能幫助到遇到類似問題的開發者。
