📝

2万超のコンテンツを抱える大規模サイトをWordPressからmicroCMSに移行した記録

2024/02/14に公開

はじめに

プラスクラス・スポーツ・インキュベーション株式会社 エンジニアキャプテンの GORAN です。

本記事は、実務案件(某プロスポーツクラブ様の公式サイトフルリニューアル)で実施した大規模なコンテンツ移行の記録です。

このサイトリニューアルは microCMS 様の事例紹介コンテンツにも掲載いただきましたので、そちらもぜひご覧ください。

https://blog.microcms.io/usecase-cerezo/

前提

  • 移行データは WordPress のエクスポート機能で生成された XML
  • XML→JSON の変換は PHP
  • JSON→microCMS の POST は POST API を Node.js から使用
  • サンプルコードはすべてニュース記事コンテンツについてのもの

構成

  • コンテンツデータの管理:microCMS
  • 過去のメディアデータのホスティング:Firebase Hosting
  • サイト自体のホスティング:Vercel
  • フロントエンド:Next.js (Pages Router)

手順

まずは以下の記事に目を通しましょう。大筋の流れは一緒です。

https://zenn.dev/kandai/articles/f6a034d166e4c977a78e

XML のエクスポート

WordPress の管理画面ツール → エクスポートから簡単に生成できます。
ただし、ファイルを分割してエクスポートできないのが難点です。
100MB を超える投稿タイプも少なくなかったので、そこそこ手元の環境にメモリがないとファイルをエディタで開くのも大変だったろうと思います。

https://wordpress.com/ja/support/export/

API スキーマ(JSON)の定義

microCMS 自体の立ち上げの HOW TO はドキュメントがたくさんあるので割愛します。(公式のチュートリアルなど)本記事ではスキーマ定義の方針について記述します。

microCMS には非常に多くのスキーマが用意されています
特に

  • コンテンツ参照
  • カスタムフィールド
  • 繰り返しフィールド

は強力で、これらの組み合わせでコンテンツサイトに必要なリレーションはほぼ表現できると思います。

また、microCMS の API スキーマは JSON ファイルをインポートすることでも定義できます
対象のサイトは WordPress に ACF を導入していたので、Export As JSON を使って JSON ファイルを出力することが容易でした。

方針

  • リニューアル前後で変わらないフィールドについてはそのまま
  • リニューアルの前後で型が変わるフィールドについては、スキーマ自体は引き継がず、GUI から再定義する
  • フロントで例外処理するために、移行コンテンツが存在する場合はフラグ(is_migrated)を立てる

リニューアル前後で変わらないフィールドについては、ACF からエクスポートした JSON を ChatGPT 経由で microCMS のスキーマ形式に変換することで、かなり時短になったと思います。
リニューアルの前後で型が変わるフィールドについては、スキーマ自体は引き継がず、GUI から再定義する方法を取りました。
また、移行コンテンツ(WordPress のデータ)が存在し、かつ、今後は新環境(microCMS)でコンテンツが増えていくデータについては、移行コンテンツにis_migratedのフラグを立てることで、Next.js 側で表示を棲み分けられるようにしました。

JSON

スキーマが定義できれば、microCMS の API プレビュー(POST)からリクエストに必要な JSON 形式のデータを確認できます。この形式の JSON を用意できれば POST API で移行できるということです。

microCMS POST APIのプレビュー
{
  "title": "テキスト1",
  "date": "[YYYY-MM-DDTHH:MM:SS.MS]Z",
  "category": [
    "参照先id1",
    "参照先id2"
  ],
  "contents": "複数行のテキストを入力\n複数行のテキストを入力",
  "is_html": true,
  "html_contents": "複数行のテキストを入力\n複数行のテキストを入力",
  "is_migrated": true,
}

メディアファイルの移行

WordPress のメディアアップロード機能(/wp-content/uploads/配下)に大量の画像・ファイルデータを保持していました。
冒頭で紹介した記事ではコンテンツデータ内のパスを書き換える手法を取っていましたが、今回は違うやり方で対応しています。
というのも、画像ファイルの一括アップロードについて、microCMS のメディア機能の仕様変更があり、アップロード後の URL をファイル名から一致させることが困難になってしまったためで、本記事の案件では uploads ディレクトリ配下の構造を保ったまま静的ファイル用のサーバー(Firebase Hosting)にアップし、Next.js のリダイレクト機能を利用することにしました。

Firebase Hosting の設定

  • publicフォルダの下を愚直にデプロイ
firebase.json
{
  "hosting": {
    "public": "public",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"]
  }
}

静的ファイルをホストするだけの環境なので、公式ドキュメントにある以上のことはしていません。カスタムドメインも接続せず、デフォルトのドメイン(****.web.app)のまま運用しています。

Next.js  の設定

next.config.js
const nextConfig = {
  // 省略
  images: {
    domains: [
      'images.microcms-assets.io', // microCMSのメディアファイルを読み込めるように
      '****.web.app', // Firebase Hostingで公開されているファイルを読み込めるように
      // 省略
    ],
  },
  async redirects() {
    return [
      {
        source: '/upload/:slug*',
        destination: 'https://****.web.app/upload/:slug*',
        permanent: true,
      },
      // 省略
    ]
  }
  // 省略
}

microCMS への画像の一括アップロードについては、API 経由で画像アップロードができるようになったみたいなので、もし今その手法を採用するなら API で対応する気がします。

旧サーバーからメディアファイルをダウンロード → Firebase Hosting へアップロード の過程で、ファイル名の文字エンコードに関する問題にも直面したので、別記事で紹介できればと思います。

XML→JSON の変換

エクスポートした XML は大体以下のような形をしているはずです。
<item>のみ抜粋しています

WordPress.YYYY-MM-DD.xml
<item>
    <title><![CDATA[ITEM_TITLE]]></title>
    <link>[ITEM_URL]</link>
    <pubDate>D, d M Y H:i:s +0900</pubDate>
    ...省略
    <content:encoded><![CDATA[]]></content:encoded>
    <wp:post_id>[ITEM_ID]</wp:post_id>
    ...省略
    カスタムフィールドがある場合は以下のような形になる
    <wp:postmeta>
        <wp:meta_key><![CDATA[ITEM_META_KEY]]></wp:meta_key>
        <wp:meta_value><![CDATA[ITEM_META_VALUE]]></wp:meta_value>
    </wp:postmeta>
    ...省略
</item>

この XML を JSON へ変換するのに本記事の案件では PHP を使用しました。PHP を採用した理由は特になく(XML を処理させたい PC に PHP がインストールされていたから程度)、xml2jsfsなどを用いれば Node.js でも同様に実装できます。

ここでひとつ注意があって、

解決方法としては

  • $obj->{'hoge:element'}として参照する
  • メンバ変数の:_に置き換える(_で繋がっているメンバ変数はアロー演算子でアクセスできる)

のふたつが考えられます。今回は前者で対応しました。

以上を踏まえれば、

ChatGPT
`XMLの<item>をコピペ`
をもとに
`スキーマ定義後のJSONをコピペ`
に変換する処理をPHPで実装

のようなプロンプトで ChatGPT から生成されたプログラムをほとんどそのまま使用できます。
以下は、上述の注意点も加味しながら修正したもので、3-4 回のやり取りでほぼ仕様を満たすものを書き上げてくれました。

PHP
<?php
$xml = "./[FILE_NAME].xml";
// Load the XML file and create a SimpleXML object
$xmlData = simplexml_load_file($xml, 'SimpleXMLElement', LIBXML_NOCDATA);

// Initialize an empty array to store the JSON data
$jsonData = array();

// Loop through the <item> elements
foreach ($xmlData->channel->item as $item) {

    $wp = $item->wppostmeta;
    // Create an empty array to store the item data
    $itemData = array();

    // Add the item data to the array
    $itemData['id'] = (int) $item->{'wp:post_id'};
    $itemData['title'] = (string) $item->title;
    $itemData['link'] = (string) $item->link;
    $itemData['pubDate'] = (string) $item->pubDate;
    $itemData['html_contents'] = (string) $item->{'content:encoded'};

    // Initialize an empty array to store the categories
    $itemData['categories'] = array();
    // Loop through the <category> elements and add them to the array
    foreach ($item->category as $category) {
        $itemData['categories'][] = (string) $category;
    }

    // Initialize an empty array to store the postmeta data
    $itemData['postmeta'] = array();
    // Loop through the wp:postmeta elements and add them to the array
    foreach ($item->{'wp:postmeta'} as $postmeta) {
        $metaData = array();
        $metaData['metaKey'] = (string) $postmeta->{'wp:meta_key'};
        $metaData['metaValue'] = (string) $postmeta->{'wp:meta_value'};
        $itemData['postmeta'][] = $metaData;
    }

    // Add the item data to the JSON data array
    $jsonData[] = $itemData;
}

// Convert the JSON data array to a JSON string
$jsonString = json_encode($jsonData);

// Output the JSON string
echo $jsonString;

JSON ファイルを POST

JSON ファイルを作成できれば、POST 自体はごく単純で、ファイルを読み込んでオブジェクトの配列を生成し、ループ処理で POST し続けるというものになります。

利用したオプション

Node.js
const axios = require('axios')
const fs = require('fs')

const dataArray = JSON.parse(fs.readFileSync('./[FILE_NAME].json', 'utf-8')) // JSONファイルのパスを指定

const postRequests = async () => {
  for (let data of dataArray) {
    try {
      const response = await axios({
        method: 'post',
        url: 'https://[SERVICE_ID].microcms.io/api/v1/**', // エンドポイント
        headers: { 'X-MICROCMS-API-KEY': '[API_KEY]' }, // APIキー
        data,
      })
      console.log(response)
    } catch (error) {
      console.error(error)
    }
  }
  console.log('All POST requests have been completed.')
}

postRequests()

表示の結合

  • microCMS から GET したデータをもとにis_migratedを処理
  • is_migratedに応じて、表示するコンテンツデータと適用するスタイルを棲み分け
/news/[id].tsxの一部
const contents = is_migrated ? html_contents : rich_editor
...

return (
  ...
  <div
    className={`${styles.article} ${is_migrated ? styles.migrated : ''}`}
    dangerouslySetInnerHTML={{
      __html: contents,
    }}
  />
  ...
)

...

おわりに

  • microCMS はとても良くできたサービス
  • ChatGPT と Copilot のおかげで WordPress → microCMS のようなリニューアル前後でデータベース基盤が変わる移行もかなりラクになってるので、別案件でも積極的に移行を推奨したい
    • 特に WordPress のバージョン管理を徹底できていないサイト(5 系以前からアップグレードできていないようなサイト)は microCMS x ウェブフレームワーク に置き換えたい

あまり類を見ない移行方法だと思っていますが、自分が取り組む過程ではフィットする前例をなかなか見つけられず苦労したので、この記録がどこかでニッチなことに取り組もうとしている誰かの何かの役に立つことを願っています。

GitHubで編集を提案
プラスクラス・スポーツ・インキュベーション株式会社

Discussion