WordPress で動いているサイトを、別のCMSへ移そうとしている開発者向けの話です。移行の見積もりを「記事が300本あるから」で出すと外れます。外れる理由は本文の保存形式にあるので、そこから順に書きます。
確かめた環境は emdash 0.32.0 です。Markdown を Portable Text へ変換する部分(node_modules/emdash/dist に入っているもの)を直接呼び出して、WordPress の本文の形をそのまま通しました。WXR ファイルを丸ごと取り込む実験はしていません。ここに書くのは、本文を1本通したときに何が残るかまでです。
WordPress の本文は、表示用のHTMLではない
ブロックエディタが保存する post_content には、HTMLに混じって <!-- wp:image --> のようなコメントが入ります。WordPress 公式のブロックエディタハンドブックは、この文字列の位置づけをこう書いています。
A block editor post is not the artifact it produces, namely the post_content. The latter is the printed page, optimized for the reader but retaining its invisible markings for later editing.
編集用の印を残したまま印刷された紙、という言い方をしています。なぜHTMLコメントなのかも同じページに書かれていて、コメントはHTMLの属性の中のような曖昧な場所に存在できず、ブラウザが無視し、パーサがHTMLを完全に理解しなくても先に取り出せるからだと説明されています。
ショートコードも同じ場所に同居します。公式のプラグインハンドブックは、本文の中でPHPを走らせることが安全上禁じられていて、その代わりとして 2.5 で導入されたと書いています。[gallery] のような文字列を本文に置き、表示のたびに登録された関数が中身に差し替えます。
つまり post_content は、読者に見せるHTMLと、編集や表示のための指示が、1本の文字列に同居した状態です。取り出す側から見ると、HTMLとして読めば指示が邪魔になり、指示として読めばHTMLが邪魔になります。
エクスポートに入るのは、その同居した文字列そのもの
WordPress の書き出し機能が作るのは WXR(WordPress eXtended RSS)というXMLです。公式のドキュメントは、投稿・固定ページ・コメント・カスタムフィールド・分類・ユーザーを含むと書いたうえで、その使い道を「別の WordPress サイトの管理画面からインポートする」と説明しています。
移送先も WordPress である前提の形式です。だから本文は加工されず、ショートコードもブロックコメントも書かれた形のまま出てきます。
WordPress から WordPress へ移すぶんには、これで困りません。同じプラグインを入れれば同じように解釈されます。壊れるのは、移行先が WordPress ではないときです。
解釈されなかった行は、段落になる
移行先を EmDash(Cloudflare のCMS)にした場合を見ます。EmDash の本文は Portable Text という形式で、仕様は本文をブロックとスパンの配列として定義しています。表がどう扱われるかは、仕様の「Custom blocks」の節に書かれています。
Custom blocks are typically images (for inline images, see the marks section), code blocks, tables, video embeds, or any data structure.
表は仕様が型を決めておらず、各プラットフォームが自分で定義するものとして置かれています。定義していなければ、表を入れる先はありません。
実際に何が起きるかを見るため、WordPress の本文にありそうな形を作って変換器に通しました。
import { r as markdownToPortableText } from "emdash/dist/portable-text-CNahFfj6.mjs";
const sample = `## 料金表
<!-- wp:table -->
| プラン | 月額 |
| --- | --- |
| 無料 | 0円 |
<!-- /wp:table -->
[contact-form-7 id="42" title="お問い合わせ"]
<!-- wp:latest-posts {"postsToShow":4,"displayPostDate":true} /-->
通常の段落と **強調** と [リンク](https://example.com)。`;
for (const b of markdownToPortableText(sample)) {
const text = (b.children ?? []).map((c) => c.text).join("");
console.log(`[${b._type}/${b.style ?? ""}] ${JSON.stringify(text)}`);
}出力です。
[block/h2] "料金表"
[block/normal] "<!-- wp:table -->"
[block/normal] "| プラン | 月額 |"
[block/normal] "| --- | --- |"
[block/normal] "| 無料 | 0円 |"
[block/normal] "<!-- /wp:table -->"
[block/normal] "[contact-form-7 id=\"42\" title=\"お問い合わせ\"]"
[block/normal] "<!-- wp:latest-posts {\"postsToShow\":4,\"displayPostDate\":true} /-->"
[block/normal] "通常の段落と 強調 と リンク。"見出しは h2 になり、最後の段落の **強調** と [リンク](url) はマークとして正しく取り込まれています。それ以外のWordPress由来の記法は、すべて normal、つまりただの段落です。しかも中身は元の文字列そのままです。
変換器の実装を読むと理由が分かります。この変換は行単位で、見出し・引用・箇条書き・コードフェンス・画像のいずれのパターンにも当てはまらない行を、そのまま段落にします。当てはまらなかった行を落とすのではなく、文字として残す作りです。
だから移行しても記事は消えません。消えない代わりに、読者から見えるところに [contact-form-7 id="42"] という文字列が出ます。この記事を書いているブログ自体が EmDash で動いていて、本文に表を書くと同じことが起きます。
何が移って、何が移らないか
上の実測から整理します。
- 見出し・段落・箇条書き・引用・強調・リンク: 移ります。ここが記事の本体なので、記事数が多いこと自体は問題になりません
- 画像: 行全体が
の形なら画像ブロックになります。本文中に<figure>として埋め込まれている場合は段落になります - 表: 移りません。行ごとに
| A | B |という段落が並びます - ショートコード: 移りません。
[foo id="1"]という段落になります - ブロックコメント: 移りません。
<!-- wp:latest-posts {...} /-->という段落になります - プラグインが表示のたびに描いていたもの: 新着記事一覧・問い合わせフォーム・会員限定の表示など。本文には元から実体がありません
最後の行が見落とされやすいところです。動的ブロックの本文に入っているのは「4件表示する」という指示だけで、記事の中身は保存されていません。移す対象がそもそも無いので、移行先で作り直すか、要るかどうかから決め直すことになります。
見積もりで数えるのは、記事数ではない
移行の作業量は、ショートコードやブロックの種類の数で決まります。同じショートコードが500記事に出てきても、置き換えの規則を1本書けば全部片づきます。逆に記事が20本でも、使われているショートコードが12種類あれば、12回の判断が要ります。
だから最初に数えるのはこの3つです。
- 本文に出てくるショートコードの種類。WXR を書き出して
grep -o '\[[a-z0-9_-]\{2,\}' export.xml | sort -uで一覧になります - 動的ブロックの種類。
grep -o '<!-- wp:[a-z0-9/-]\+' export.xml | sort -uで出ます - 表を含む記事の本数
数えた結果、作り直しになるものが本文の中心を占めているなら、移行より新しく作るほうが安く済みます。その判断を、記事を1本も動かす前にできます。
書いていないことも残しておきます。WXR を丸ごと読み込んで変換するツールは作っていません。ショートコードを移行先の形に置き換える規則も、プラグインごとに違うので一般化できていません。ここで示したのは、移行に入る前に見積もりの母数を正しく取るところまでです。