はてなブログのAtomPub APIで予約投稿の時間だけ変えたら、カテゴリが1つに減って本文も古い版に巻き戻った。原因はPUTが「部分更新」ではなく「全体置換」だから。安全に時間だけ変えるには、GETで最新XMLを取得→<updated>だけ差し替え→PUTする。
何が起きたか
はてなブログの予約投稿、公開タイミングをずらしたくなったんですよ。AtomPub APIで <updated> を書き換えてPUTすればいいかなと。
やってみたら200 OKが返ってきた。成功したと思ってブログを開いたら、カテゴリが3つあったはずなのに1つに減ってる。しかもWebUIで加筆した本文が、ローカルに保存してあった古い版で上書きされてた。
「時間を変えただけ」のつもりが、記事の中身まで巻き戻ってた。
原因
AtomPub(RFC 5023)のPUTは「部分更新」ではなく「リソース全体の置換」です。
RFC 5023でも「クライアントは意図的に変更していないメタデータを保持すべき(SHOULD)」と明記されています(Section 9.3)。
ローカルでXMLを組み立ててPUTすると、こうなります:
ローカルの状態: title: "記事タイトル" content: ローカルに保存された本文 category: ["タグA"] ← 1つだけ updated: "2026-03-20T09:00" ← 変更したい値 リモートの状態(WebUIで編集済み): title: "記事タイトル(修正済み)" content: WebUIで加筆した本文 category: ["タグA", "タグB", "タグC"] updated: "2026-03-18T09:00" PUT結果: 200 OK ... だがリモートがローカルで上書き → カテゴリが3つ→1つに → 加筆した本文が消える
200が返ってくるので一見成功に見える。壊れたことに気づくのはブログを開いたとき。
400エラーで止まってくれるならまだいいけど、「成功するけど中身が壊れている」パターンが厄介です。
問題を正しく分解すると:
- ローカルで組み立てたXMLが、リモートの最新状態を反映していない
- PUTは送信したXML全体でリソースを置き換えるので、古い情報が「正」として上書きされる
「変更手段」の問題ではなく「変更のベースライン」の問題です。
対策: GET→差替→PUTパターン
リモートの最新XMLをGETして、変えたい箇所だけ差し替えて、そのままPUTする。
# 1) リモートの最新Entryを取得
ENTRY_XML=$(curl -s -u "${HATENA_USER}:${HATENA_API_KEY}" \
"https://blog.hatena.ne.jp/${HATENA_USER}/${HATENA_BLOG_DOMAIN}/atom/entry/${ENTRY_ID}")
# 2) <updated> の値だけ差し替え
NEW_TIME="2026-03-20T09:00:00+09:00"
UPDATED_XML=$(echo "$ENTRY_XML" | sed "s|<updated>[^<]*</updated>|<updated>${NEW_TIME}</updated>|")
# 3) そのままPUT
echo "$UPDATED_XML" | curl -s -X PUT \
-H "Content-Type: application/atom+xml;type=entry" \
-u "${HATENA_USER}:${HATENA_API_KEY}" \
--data-binary @- \
"https://blog.hatena.ne.jp/${HATENA_USER}/${HATENA_BLOG_DOMAIN}/atom/entry/${ENTRY_ID}"
ポイントは「ローカルのファイルからXMLを組み立てない」こと。リモートから取得したXMLをベースにするから、WebUIで編集した内容もカテゴリも全部保持されます。
なお、GETしたXMLに <hatenablog:scheduled>yes</hatenablog:scheduled> が含まれていない場合(古い記事など)は、PUTするXMLに追加が必要です。
これがないと予約状態が解除される可能性があります。
注意: 公開済み記事をrescheduleしない
もう1つ見落としやすい罠があります。
公開済み記事(app:draft = no)に対して <updated> を未来日時に変えてPUTすると、記事が app:draft = yes に戻ります。公開中の記事が非公開になる。
対策として、GETした時点で app:draft=yes であることを確認してから処理を進めます。
# ガード: 下書き状態でなければ中断 if ! echo "$ENTRY_XML" | grep -qE '<app:draft>\s*yes\s*</app:draft>'; then echo "ERROR: この記事は公開済みです。rescheduleすると非公開に戻ります。" >&2 exit 1 fi
まとめ
AtomPub APIのPUTは全体置換。「1箇所だけ変えたい」ときでも、ベースラインはリモートの最新状態でなければなりません。GET→差替→PUTを定型パターンにしておくと、「時間だけ変えたのに本文が消えた」類の事故は構造的に起きなくなります。
参考
- はてなブログAtomPub - はてなブログ ヘルプ — AtomPub APIの公式リファレンス
- RFC 5023: The Atom Publishing Protocol — Section 9.3にPUTによるエントリ編集の規定
- 「はてなブログAtomPub」ではてなブログへの新規投稿とアップデート — PUT時のXML構造の実践例
- kymmt90/hatenablog - GitHub — AtomPub APIのRubyラッパー、update_entryの実装が参考になる