起きたこと
WordPress の REST API で記事を作成するとき、status に future、date に未来の日時を指定した。予約投稿のつもりだった。
レスポンスは 200。投稿 ID もタイトルも返ってくる。エラーはひとつも出ていない。
それなのに、管理画面を開くと記事は 公開済み になっていた。
結論
WordPress が「もう公開していい時刻か」を判定するのに見ているのは date ではなく date_gmt です。date だけを未来にして date_gmt を放置すると、即時公開されます。
なぜそうなるのか
WordPress の投稿は日時を2つのカラムで保持しています。
| カラム | 内容 |
|---|---|
post_date / API の date
|
サイトのローカル時刻(日本語環境なら JST) |
post_date_gmt / API の date_gmt
|
UTC |
管理画面の日時ピッカーで人間が触るのは前者ですが、公開判定に使われるのは後者です。
そのため date にだけ未来の値を入れて date_gmt を送らない(あるいは古い値のまま残す)と、
- ローカル時刻の予定 → まだ未来
- UTC の予定 → すでに過去
という食い違いが生まれ、WordPress は date_gmt 側を採用して「公開時刻を過ぎている」と判断します。結果、future が publish に落ちます。
JST は日付ごと変わるので事故りやすい
JST は UTC+9 です。素朴に「9時間ずらすだけ」と考えていると、日付をまたぐケースで踏み外します。
JST: 2026-09-17 07:00:00
UTC: 2026-09-16 22:00:00 ← 時刻だけでなく "日付" が前日になる
朝の時間帯に予約したい場合、UTC 側はほぼ確実に前日になります。ここを手で書いていると、いつか必ずズレます。
対処
「公開したい日時」をひとつ決めたら、そこから date と date_gmt を同じロジックで機械的に生成して、必ず両方送る。これだけです。
{
"status": "future",
"date": "2026-09-17T07:00:00",
"date_gmt": "2026-09-16T22:00:00"
}
実装面では、日時変換を1か所に閉じ込めるのが効きました。呼び出し側それぞれで日時を組み立てられるようにしておくと、どこかで date_gmt を書き忘れる経路が必ず生まれます。変換関数はひとつだけ用意して、他の場所は変換済みの値を受け取るだけにしておくと、書き忘れようがなくなります。
検知:送信の前後に1回ずつ確認する
変換を正しく実装しても、それだけでは事故に気づけません。API がエラーを返さないためです。自分は前後に検査を挟んでいます。
送信前
指定日時が現在時刻より十分に未来かを見ます。「未来かどうか」のブール判定だけだと足りません。現在時刻との差がほぼゼロの日時を送ると、変換が正しくてもリクエストが処理されるまでのラグで「もう過去」の扱いになりえます。数分のバッファを持たせます。
BUFFER = 5分
if (target <= now + BUFFER) {
// 送らずに中断する
}
送信後
作成・更新のレスポンスを信用せず、別リクエストで同じ投稿を取り直して3点を照合します。
confirm = GET /wp-json/wp/v2/posts/{id}
assert confirm.status == "future"
assert confirm.date == 送信した date
assert confirm.date_gmt == 送信した date_gmt
片方だけでは不十分です。送信前チェックだけでは実際に DB に入った値を確認できませんし、送信後チェックだけでは事故ってから気づくことになります。
補足:テストでは再現しないことがある
このバグは、テストの実行時刻と予約したい日時が近いと表面化しません。date_gmt を更新し忘れていても、たまたま UTC 換算がまだ未来の範囲に収まっていれば正常に予約されるからです。
「ローカルで試したら動いた」で切り上げると、本番で時刻差の大きいケースを踏んだときに初めて発覚します。上の送信後チェックまでを1セットにしておくのが確実です。
まとめ
| 症状 |
status: future で送ったのに即 publish になる |
| 原因 |
date のみ更新し date_gmt が古い/未送信 |
| 判定に使われる値 |
date_gmt(UTC)のほう |
| 罠 | JST→UTC は日付をまたぐ(JST 07:00 = UTC 前日 22:00) |
| 対処 |
date と date_gmt を同一ロジックで生成し同時送信 |
| 検知 | 送信前に未来判定+バッファ、送信後に GET で読み返し |
なお、同じ「予約投稿がうまくいかない」でも、公開が遅れる症状のほうは原因がまったく別(WP-Cron がアクセス契機で動くこと)です。混同すると調査の方向を間違えるので、症状が「早すぎる」のか「遅すぎる」のかを最初に切り分けると早いです。