「公開済み」なのに本文が消えた話 — WordPress REST APIとwp:htmlブロックの罠

きっかけ: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コメントは保持されたまま保存された。

確認すると、文字数・ブロックコメントの有無・新しい要目データ、すべて正しく反映されていた。

学びのまとめ

  1. 「成功」のレスポンスを信用しすぎない。POSTが200を返しても、タイトルが正しく返ってきても、本文が本当に反映されたかは別途確認する必要がある。
  2. 二分探索は効く。境界がどこにあるか分からないバグは、内容を疑う前に「どこまでなら通るか」を機械的に絞り込むと、思い込みに頼らず原因に近づける。
  3. 見た目が正しく見えても、構造は壊れているかもしれない。フロント側のレンダリングは「たまたま動いて見える」状態を作り出すことがある。エディタ側(管理画面)での見た目の確認は、フロント側だけのチェックでは気づけない問題を拾ってくれる。
  4. 同じ操作でも「やり方の形」が結果を左右することがある。今回は「新規代入」か「既存への置換」かという、見た目には大差ないはずの操作の違いが、内部処理の分岐点になっていた。

壊れた記事を直すつもりが、直す手段そのものの仕組みを理解する作業になった。結果的に、次回以降の記事修正はずっと安全に進められるようになった。これもまた、構築と解体の一場面だったのだと思う。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です