きっかけ:PowerShellから記事を更新したら、デザインが消えた
WordPressの記事を1件修正したかった。たったそれだけだった。
艦艇データベース化プロジェクトの一環で、既存の戦史記事に欠けていた要目データ(起工日・竣工日・排水量など)を追記する作業をしていた。記事のGutenbergエディタがInternal Server Errorで開けなくなっていたので、REST API経由でPowerShellから直接本文を更新する方法に切り替えた。
最初の1件(敷波の記事)はすんなりいった。本文を取得し、テーブルの該当行を文字列置換で差し込み、POSTする。確認すると、要目データはきちんと反映されていた。
次の1件(深雪の記事)でハマった。
この記事は本文が壊れていて、'Army',という意味不明な1行しか残っていない状態だった。なので今回は「差し替え」ではなく「ゼロから新しいHTMLを作って丸ごとPOST」する方式を取った。タイトルもステータスも正しく返ってきたので、成功したと思っていた。
ところが、保存後にもう一度本文を取得して確認すると——'Army',の1行に戻っていた。何も変わっていなかった。
仮説1:コピペミスでは? → 違った
最初に疑ったのは単純な操作ミスだった。メモ帳にHTMLを貼り付ける作業を挟んでいたので、貼り付け漏れの可能性を疑い、PowerShellの中で直接文字列処理をする方式に切り替えた。
$response2 = Invoke-RestMethod -Uri ".../posts/14188?context=edit" -Headers $headers -Method Get
$content = $response2.content.raw
$newContent2 = $content -replace [regex]::Escape($oldText), $newText
差し替え自体はちゃんと動いた(マーカー出現回数1、文字数も予定通り増加)。POSTも「成功」と返ってきた。
しかし確認すると、また本文が反映されていない。コピペミスではなかった。
仮説2:文字数制限では? → 半分正解、半分不正解
ここで二分探索を始めた。本文を半分・4分の1・8分の1……と切り出してPOSTし、どこまでの長さなら通るかを調べた。
$miyukiPartial = $miyukiNewContent.Substring(0, 3000)
# → 成功(3025文字保存)
$miyukiHalf = $miyukiNewContent.Substring(0, [Math]::Floor($miyukiNewContent.Length / 2))
# → 成功(8876文字保存)
# ...17107文字でも成功、17590文字でも成功...
# しかし完全な17659文字(全文)だけが失敗する
3000文字でも、半分でも、75%でも、87.5%でも成功する。なのに全文(完全に閉じたHTML)だけが失敗する。これは直感に反する結果だった。文字数の上限を超えているわけではない。「最後まで完全に閉じている」ことそのものが、何かのトリガーになっているように見えた。
仮説3:<!-- wp:html -->ブロックコメントが原因? → 当たり
最後の69文字を見てみると、ごく普通の閉じタグの羅列だった。
</li>
</ul>
</div>
</div>
<!-- /wp:html -->
特殊文字も絵文字もない。ただの</div>と、Gutenbergのブロック終端コメントだけ。
ここで、思い切って<!-- wp:html -->と<!-- /wp:html -->を本文から除去してPOSTしてみた。
$miyukiNoBlockComments = $miyukiNewContent -replace "<!-- wp:html -->", "" -replace "<!-- /wp:html -->", ""
結果、完全に保存された。17626文字、すべて反映。
これでようやく「動いた」と思った。原因はwp:htmlブロックコメントだ、と。
しかし、それは別の問題を生んでいた
確認のためフロント側のページを見ると、CSSも色もレイアウトも正常に見えた。安心して報告した。
ところが、サイト管理者から「他の記事と見栄えが違う」という指摘が来た。
並べて見ると、原因は一目で分かった。
- 正常な記事:投稿タイトルが表示されず、記事本文のヒーローセクションがそのまま画面の最上部から始まる
- 問題の記事:WordPressの投稿タイトルが大きな白文字でそのまま表示され、その下に記事本文が続く
さらにGutenbergのビジュアルエディタを開いて比較すると、違いがもっと明確になった。
- 正常な記事:1個の「カスタムHTMLブロック」として認識され、エディタ上でもデザイン通りにレンダリングされる
- 問題の記事:HTMLタグがそのままテキストとして1行ずつ「段落ブロック」に分解されてしまっている
<!-- wp:html -->を除去したことで、たしかに文字数の問題は回避できた。しかしその代償として、Gutenbergが「これはカスタムHTMLブロックだ」と認識する手がかりそのものを失わせていた。フロント側でCSSが効いて見えたのは、ブラウザが段落ブロックの中に残った生のHTML文字列をたまたま解釈してくれていただけで、構造としては壊れていた。
直さなければいけない問題は、最初から変わっていなかった。「wp:htmlブロックを保ったまま、長い本文を保存する」こと。
本当の原因:「新規投入」と「差分置換」で挙動が違う
ここで振り返って気づいたのが、最初に成功していた「敷波」のケースとの違いだった。
- 敷波:既存の本文を取得し、その一部分だけを文字列置換してPOST →
wp:htmlコメントは保持された - 深雪:まったく新しいコンテンツを
contentにそのまま代入してPOST →wp:htmlコメントが消えた
試しに、敷波の現在の状態を確認してみた。
$shikinamiCheck = Invoke-RestMethod -Uri ".../posts/14188?context=edit" -Headers $headers -Method Get
$shikinamiCheck.content.raw.Contains('wp:html')
# → True
予想通りだった。同じサーバー、同じ投稿タイプ、同じ認証。違いは「POSTするcontentの作り方」だけ。
- 全体を新規に代入する形 → コメントが消える
- 既存の内容に対して
-replace操作を行う形 → コメントが保持される
これがWordPress側の内部実装(リビジョン管理かサニタイズ処理)に起因するものなのか、明確な仕様文書は見つけられていない。だが再現性は確認できた。
解決策:常に「既存の本文」を経由して置換する
最終的に採用した方法はこうだ。
# 壊れている本文も、まず一度取得する
$current = Invoke-RestMethod -Uri ".../posts/13034?context=edit" -Headers $headers -Method Get
$oldFullContent = $current.content.raw
# 新しい完全な本文(wp:htmlコメント込み)を用意
$newFullContent = "<!-- wp:html -->`n" + $htmlBody + "`n<!-- /wp:html -->"
# "新規代入" ではなく "置換" の形にする
$finalContent = $oldFullContent -replace [regex]::Escape($oldFullContent), $newFullContent
$body = @{ content = $finalContent } | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri ".../posts/13034" -Headers $headers -Method Post -Body $body -ContentType "application/json; charset=utf-8"
$oldFullContent全体を検索パターンにして、それを$newFullContentに置き換える——内容としては「全文入れ替え」だが、PowerShell上の操作としては「置換」の形を取っている。これだけで、wp:htmlコメントは保持されたまま保存された。
確認すると、文字数・ブロックコメントの有無・新しい要目データ、すべて正しく反映されていた。
学びのまとめ
- 「成功」のレスポンスを信用しすぎない。POSTが200を返しても、タイトルが正しく返ってきても、本文が本当に反映されたかは別途確認する必要がある。
- 二分探索は効く。境界がどこにあるか分からないバグは、内容を疑う前に「どこまでなら通るか」を機械的に絞り込むと、思い込みに頼らず原因に近づける。
- 見た目が正しく見えても、構造は壊れているかもしれない。フロント側のレンダリングは「たまたま動いて見える」状態を作り出すことがある。エディタ側(管理画面)での見た目の確認は、フロント側だけのチェックでは気づけない問題を拾ってくれる。
- 同じ操作でも「やり方の形」が結果を左右することがある。今回は「新規代入」か「既存への置換」かという、見た目には大差ないはずの操作の違いが、内部処理の分岐点になっていた。
壊れた記事を直すつもりが、直す手段そのものの仕組みを理解する作業になった。結果的に、次回以降の記事修正はずっと安全に進められるようになった。これもまた、構築と解体の一場面だったのだと思う。