mcpField
C
contentstack-migration-companion

contentstack-migration-companion

github.com/contentstack開発565 回インストール1 週間前に更新
リポジトリ

contentstack-migration-companion について

Contentful から Contentstack へのプロジェクト移行を案内するワークフロースキルです。コンテンツを先に移行し、その後ウェブサイトのコードを扱い、手順間で成果物を引き継ぎます。アカウント操作やコード変更の前には確認を求めます。

主な機能

  1. 01Contentful コンテンツ移行の案内
  2. 02後続のウェブサイトコード移行
  3. 03順次の成果物引き継ぎ
  4. 04セッションごとの作業領域
  5. 05重要な手順前の確認

活用シーン

  1. 01コンテンツタイプとエントリーを Contentstack に移行
  2. 02コンテンツ移行後にサイトの CMS 読み取りコードを変更
  3. 03各手順で一貫した移行結果を引き継ぐ
  4. 04ログインやコード編集の前に確認を取る

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/contentstack/contentstack-agent-skills --skill contentstack-migration-companion

インストール後、Claude Code で/contentstack-migration-companionを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
CONTENTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md
scripts/
_lib.sh
01_contentful_residue.sh
02_field_access.sh
03_sdk_init.sh
04_query_builder.sh
05_references.sh
06_richtext.sh
07_assets.sh
08_locales.sh
09_graphql.sh
10_livepreview.sh
11_build_typecheck.sh
12_secrets.sh
13_todos_report.sh
check_prereqs.py
log.sh
parse_import_summary.py
README.md
run-all.sh

詳細

contentstack-migration-companion

ContentfulからContentstackへのプロジェクト移行をユーザーに案内します。 まず、Contentstack CLI migrate プラグインを使ってコンテンツ(コンテンツタイプ、エントリー、アセット、ロケール)を移行し、次にCMSからデータを読み込むウェブサイトのコードを移行します。

これは、各ステップの出力を次のステップで使用する逐次的なワークフローです(create コマンドによって、データが入力されたスタックと認証情報を含むバンドルが生成され、それをコード移行に使用します)。各コマンドが出力するアーティファクトのパスと件数は、取得して引き継ぐ必要がある状態として扱ってください。

運用原則

以下の原則は、単一のコマンド以上に重要です。ワークフロー全体を通して守ってください。

  • 一意なセッション用ワークスペースを使用します。 ステップ 1 の開始時に、次のコマンドを実行してセッションスコープのディレクトリを作成します。
    SESSION_ID=$(date +%Y%m%d-%H%M%S) && SESSION_DIR="/tmp/migrate-to-cs/$SESSION_ID" && mkdir -p "$SESSION_DIR" && echo "SESSION_DIR=$SESSION_DIR"
    
    出力された SESSION_DIR の値(例:/tmp/migrate-to-cs/20260608-143022)を記録し、この移行で実行するすべてのシェルコマンドで具体的なリテラル値として引き継いでください。再生成してはいけません。これにより、各移行が分離され、同時実行や再実行で競合が起きません。ユーザーが別のワークスペースを指定した場合は、そのパスを使用してください。
  • 同梱スクリプトとリファレンスは、このスキルと同じ場所にあります。{SKILL_DIR} を使ってパスを解決してください。 このスキルには、ヘルパースクリプト(scripts/ フォルダー)とリファレンスドキュメント(references/ フォルダー)が、この SKILL.md ファイルと同じ場所に含まれています。手順に {SKILL_DIR} と記載されている場合は、この SKILL.md を読み込んだディレクトリ、つまりスキル自体のインストールディレクトリの絶対パスに置き換えてください。固定パスを想定してはいけません。 場所は AI アシスタント、OS、スキルがプロジェクト単位でインストールされているかユーザー単位でインストールされているかによって異なります。たとえば、<project>/.claude/skills/contentstack-migration-companion、~/.claude/skills/contentstack-migration-companion、または %USERPROFILE%\.claude\skills\contentstack-migration-companion のような Windows のパスの場合があります。実際のパスを一度確認してください(この SKILL.md を読み込んだフォルダーが該当します。不明な場合は、ワークスペースとホームディレクトリで */contentstack-migration-companion/SKILL.md を検索してください)。シェルコマンドでは、冒頭で変数に設定して(SKILL_DIR="<that absolute path>")、同梱スクリプトを "$SKILL_DIR/scripts/<name>" として実行してください。同梱スクリプトは隣接するファイルの場所を自動で特定するため、絶対パスで実行すれば、現在のディレクトリに関係なく動作します。
  • セッション全体で Node のバージョンを固定します。 マシンには複数の Node バージョン(システム、Homebrew、複数の nvm インストール)があることが多く、非対話型シェルではユーザーの nvm のデフォルトより古いバージョン(例:/usr/local/bin/node の v14)が優先される場合があります。ステップ 1 の前提条件チェッカーは、インストール済みの Node のうち最も新しい Node ≥ 20を検出し、そのディレクトリを JSON の node.bin_dir として出力します。この値を具体的なリテラル値 NODE_BIN_DIR として記録し、移行の残りの作業ではすべての csdx、npm、contentful コマンドの前に指定してください。たとえば、PATH="<NODE_BIN_DIR>:$PATH" csdx migrate:create … のようにします。これにより、設定されていないシェルが先に見つけたバージョンではなく、前提条件チェックで検証された Node で CLI を実行できます。node.bin_dir がない場合(古いチェック出力の場合)は、コマンドをそのまま実行してください。
  • 一度に一つのステップを実行し、結果を示します。 各コマンドの実行後、意味のある出力(サマリーテーブル、件数、成果物のパスなど)をユーザーに提示してください。生ログを大量に見せてはいけません。ユーザーは進捗状況を確認しています。簡潔なステータスを示し、次のステップで必要となるパスや識別子を伝えてください。
  • チェックリストで移行の進捗を追跡します。 以下の [PROGRESS] トリガーが発生するたびに、次の絵文字を使って回答に進捗ブロックを出力してください:✅ = 完了、⏳ = 実行中、⬜ = 未開始。ステップ 3 の実行中の例:
    **Migration progress**
    - ✅ Step 1 — Prerequisites & inputs
    - ✅ Step 2 — Install migrate plugin
    - ⏳ Step 3 — Content Migration
    - ⬜ Step 4 — Code Migration
    
    出力するタイミングは 2 回です。(1)各ステップの開始時、(2)そのステップの評価に合格したとき。⏳ になるステップは常に 1 つだけです。ステップ 4 は、サブステップ 4.1~4.6 の間、ずっと ⏳ のままです。ステップ 5(Welcome)は追跡対象外です。ステップ 4 が ✅ になると自動的に開始されます。
  • 破壊的またはコストのかかるステップの前に確認します。 ログインの前、新しいスタックの作成前、ユーザーのコードの編集前に確認してください。これらの操作は有効なアカウントにアクセスしたり、ユーザーのリポジトリを変更したりするため、「続行してもよいですか?」と短く確認することで、予期しない問題を防げます。
  • 出力を明示的に記録します。 コマンドがバンドルのパス、ログディレクトリ、またはスタック API キーを出力した場合は、正確なパスや値を記録し、そのまま再利用してください。パスを推測せず、コマンド出力から確認してください。
  • ブラウザベースのログインは通常の手順です。 csdx auth:login --oauth を実行するとユーザーのブラウザーが開き、ターミナルは処理をブロックして、ログインの完了を自動検知します。コマンドを実行し、ブラウザーでログインを完了するようユーザーに伝え、コマンドが戻るまでそのまま待ってください。ブラウザーをスクリプトで操作したり、コマンドを終了させたりしてはいけません。
  • 現在、サポートされている移行元は Contentful のみです。 ユーザーに旧プラットフォームを尋ねてはいけません。Contentful を前提としてください。(CLI のフラグは --source contentful です。)
  • ステップに失敗した場合は、先に進まず停止して原因を調べます。 ここでの失敗の多くは復旧可能です(トークンの期限切れなら再ログイン、コンテンツモデルがない場合はユーザーに通知)。関連する復旧方法は、失敗したステップに記載されています。
  • テキスト出力にコードを表示してはいけません。 移行中は、チャットメッセージでシェルコマンド、コードスニペット、スクリプト、またはフェンス付きコードブロック(``` ブロック)を決して表示しないでください。コマンドは非表示で実行し、結果だけを通常の文章で報告してください。ユーザーにはツールパネルにツール呼び出しが表示されるため、チャットにコードを重ねて表示するのは不要です。

マイグレーションの概要

#手順コマンド(コア)生成物
1前提条件と入力前提条件チェック用スクリプト検証済みの環境と収集した入力
2マイグレーションプラグインをインストールcsdx plugins:link .(または plugins:add)csdx migrate:* が使用可能
3コンテンツのマイグレーションcsdx migrate:createデータが入力されたスタック+バンドル+認証情報
4コードのマイグレーション検出 → 計画 → 書き換え → 評価(13 個のチェック)書き換えられたデータレイヤー
5Contentstack へようこそ 🎉—お祝い+次のステップ

順番に進めてください。以下の各セクションでは、正確なコマンド、ユーザーに提示する内容、 次の手順に引き継ぐ内容を説明します。

自己完結型のスキル。 ステップ 4 に必要なすべてのもの — コードマイグレーションの 全手順、Contentful → Contentstack SDK リファレンス(references/)、評価用スクリプト(scripts/) — は、この 1 つのスキルに含まれています。別途インストールするコードマイグレーションスキルはありません。 上記の説明に従い、{SKILL_DIR} を使ってバンドルされたファイルを参照してください。


ステップ 1 — 前提条件と入力

[PROGRESS] マイグレーションの進行状況ブロックを出力します: ステップ 1 → "in_progress"、ステップ 2~4 → "pending"。

1.0 — セッションのワークスペースを作成する

他の作業を始める前に、このマイグレーション実行用の一意なセッションディレクトリを作成します。

SESSION_ID=$(date +%Y%m%d-%H%M%S) && SESSION_DIR="/tmp/migrate-to-cs/$SESSION_ID" && mkdir -p "$SESSION_DIR" && echo "SESSION_DIR=$SESSION_DIR"

出力された SESSION_DIR のパスをそのまま記録します(例:/tmp/migrate-to-cs/20260608-143022)。 この手順で $SESSION_DIR が参照されている箇所には、この具体的な値を代入してください。 再生成しないでください。アーティファクトを正しく引き継ぐため、マイグレーションのすべての手順で 同じディレクトリを使用する必要があります。

ユーザーに次のように伝えます:"セッションのワークスペースを <SESSION_DIR> に作成しました。"

1.1 — Python 3 のコマンドを検出する

この環境で適切な Python 3 コマンドを見つけるため、次を実行します。

if python3 --version 2>&1 | grep -q "Python 3"; then
  PYTHON_CMD=python3
elif python --version 2>&1 | grep -q "Python 3"; then
  PYTHON_CMD=python
else
  PYTHON_CMD=""
fi
echo "PYTHON_CMD=$PYTHON_CMD"

出力された PYTHON_CMD をそのまま記録します。 この手順で Python を呼び出す箇所では $PYTHON_CMD を代入してください。python3 や python を直接指定しないでください。

PYTHON_CMD が空の場合は、直ちに中止し、ユーザーに次のように伝えます。

"Python 3 が見つかりません。python.org またはパッケージマネージャーからインストールして ください(例:macOS では brew install python3、Ubuntu では sudo apt install python3、 Windows では python.org からインストーラーをダウンロード)。その後、もう一度お試しください。"

Python 3 が検出されるまで、この先へ進まないでください。

1.2 — 前提条件チェッカーを実行する

次のスクリプトを 1 回だけ実行します。このスクリプトは、Node.js を内部で評価し、不足している CLI(csdx、 contentful)をインストールし、Contentstack のリージョンとログイン状態、Contentful のログイン状態と スペースを確認します。すべてを一度に実行し、JSON の概要を出力します。

$PYTHON_CMD "{SKILL_DIR}/scripts/check_prereqs.py"

JSON の結果を解析し、すべてのフィールドをセッション状態として引き継ぎます。

Node の bin ディレクトリを記録します。 JSON の node.bin_dir を、具体的なリテラル NODE_BIN_DIR として取得します。概要にある「Node のバージョンを固定する」という原則に従い、 後続のすべての csdx、npm、contentful コマンドの前に PATH="<NODE_BIN_DIR>:$PATH" を付けます。 こうすることで、未設定の非対話型シェルが最初に解決する Node ではなく、チェッカーが検証した Node を使って マイグレーション全体を実行できます(チェッカーは PATH とすべての nvm インストール先を検索し、 インストール済みの Node 20 以上のうち最も新しいものをすでに選択しています)。node.bin_dir が ない場合は、コマンドをそのまま実行してください。

重大なブロッカー: スクリプトが終了コード 1 で終了した場合、Node.js が見つからないか、バージョンが古すぎます。 報告された node.version/node.path は、マシン上のあらゆる場所から見つかった最適な Node を示します。 そのため、デフォルトの PATH に新しい Node が存在しない場合でも、メッセージは正確です。直ちに中止し、 ユーザーに具体的な問題を伝えます。

  • node.error == "not_installed" → "Node.js がインストールされていません。nvm install 22 または nodejs.org からインストールして、もう一度お試しください。"
  • node.ok == false(例:node.version == "v18.x")→ "見つかった最新の Node は <version> ですが、Node 20 以上が必要です。nvm install 22 && nvm use 22 で新しいバージョンを インストールする(またはシステムの Node をアップグレードする)か、もう一度お試しください。"

Node 20 以上が確認されるまで、この先へ進まないでください。

1.3 — Contentstack のログインが不足している場合の対処(必要な場合)

JSON の cs_login.ok が true の場合は、このサブステップをスキップしてください。

cs_login.ok が false(needs_login)または cs_login.org_uid が null(needs_oauth_reauth)の場合は、 新しい OAuth ログインを開始します。

csdx auth:login --oauth

ユーザーに次のように伝えます:"ブラウザーウィンドウが開きます。そこで Contentstack にログインしてから、こちらに戻ってきてください。" コマンドが戻るまで待ちます(ブラウザーフローが完了するまで処理がブロックされます)。その後、 前提条件チェッカーを再実行(1.2 と同じコマンド)して、更新されたメールアドレスと組織 UID を取得します。

再試行後も組織 UID が不足している場合は、ユーザーに次のように伝えます:

"OAuth ログインで組織 UID が保存されませんでした。csdx auth:login --oauth を手動で再実行してから、完了したらお知らせください。再試行します。"

1.4 — Contentful のログインが不足している場合の対処(必要な場合)

JSON の contentful_login.ok が true の場合は、このサブステップをスキップしてください。

contentful login は対話式です(ユーザーが Y を押してトークンを貼り付ける必要があります)。Bash コマンドとして実行することはできません。ユーザー自身で実行するよう案内します:

"ターミナルで次のコマンドを実行してください:

contentful login

'Continue login on the browser? (Y/n)' と表示されたら、Y を押してください。 ブラウザーウィンドウが開くので、そこでサインインします。 ブラウザーログインが完了すると、ターミナルに 'Paste your token here:' と表示されます。 ブラウザーページから Management Token をコピーして貼り付け、Enter を押してください。 ログインの成功が確認されたら、こちらに戻ってきてください。"

ユーザーがログインを完了したと確認するまで待ち、その後、前提条件チェッカーを再実行して新しいセッションを取得します。

Management Token は秘密情報です。ユーザーに共有を求めたり、要約でその内容をそのまま返したり、このワークスペース内のファイルに書き込んだりしないでください。

1.5 — 前提条件の概要を表示して確認する

cs_login.ok と contentful_login.ok が両方とも true になったら、JSON の結果から概要テーブルを表示します。問題がなさそうな項目には ✅、注意が必要な項目には ⚠️ を使用します。

チェック状態詳細
Python 3✅ <$PYTHON_CMD --version output>
Node.js✅ <node.version>
Contentstack CLI (csdx)✅ <csdx.version>
Contentstack のリージョン⚙️ <cs_region.region>
Contentstack のログイン✅ <cs_login.email>組織 UID:<cs_login.org_uid>
Contentful CLI✅ <contentful_cli.version>
Contentful のログイン✅ <contentful_login.name> <contentful_login.email>

次の質問を 1 つだけします。

「すべて問題ないようです。続行しますか?それとも、開始前に変更したいことはありますか?

  • proceed — 移行を開始する
  • region — Contentstack のリージョンを切り替える
  • contentstack login — Contentstack アカウントを切り替える
  • contentful login — Contentful アカウントを切り替える」

続行する前に、ユーザーの回答を待ちます。

回答操作
"proceed" / "yes" / 任意の肯定1.6 へ進みます
"region"リージョンメニューを表示して選択を待ち、csdx config:set:region <REGION> を実行し、前提条件チェッカーを再実行して概要を再表示します
"contentstack login"csdx auth:login --oauth を実行し、ブラウザでのログインを待ってから、前提条件チェッカーを再実行して概要を再表示します
"contentful login"ユーザー自身で contentful login を実行するよう案内し(1.4 と同じ)、確認を待ってから、前提条件チェッカーを再実行して概要を再表示します

リージョン選択メニュー — ユーザーがリージョンの変更を求めた場合は、このメニューを表示し、コマンドを実行する前に選択を待ちます。

「移行先スタックはどのリージョンにありますか?

  1. AWS-NA — AWS 北米
  2. AWS-EU — AWS ヨーロッパ
  3. AWS-AU — AWS オーストラリア
  4. AZURE-NA — Azure 北米
  5. AZURE-EU — Azure ヨーロッパ
  6. GCP-NA — Google Cloud 北米
  7. GCP-EU — Google Cloud ヨーロッパ」

番号を対応するリージョンコードに変換して、次を実行します。

csdx config:set:region <REGION>   # e.g. csdx config:set:region AWS-EU

ユーザーが続行の準備ができたことを確認するまで、概要と質問を繰り返します。

1.6 — Contentful Space ID(リストから選択)

前提条件の JSON にある contentful_spaces 配列には、アクセス可能なすべてのスペースがすでに格納されています(チェッカー内で実行した contentful space list の結果から取得)。

リストにスペースがちょうど 1 つある場合は、自動的に選択してください。ユーザーに尋ねないでください。 選択したことを伝えます。

"Contentful のスペースが 1 つ見つかりました:My Marketing Site (abc123)。自動的に選択しました。"

その ID を SPACE_ID として取得し、続行します。

スペースが 2 つ以上ある場合は、番号付きメニューを表示します。 — ユーザーに Space ID の入力や貼り付けを求めないでください:

1. My Marketing Site  (abc123)
2. Developer Sandbox  (def456)
…

次のように尋ねます。

"移行するスペースはどれですか?番号を入力してください。"

回答を待ちます。 番号を Space ID に対応付け、SPACE_ID として取得します。

リストが空の場合(スペースが解析されなかった場合)は、次のように尋ねます。

"Contentful のスペースを一覧表示できませんでした。Space ID を直接入力してください。"

次に、トークンで選択したスペースにアクセスできることを確認します。

contentful space use --space-id <SPACE_ID>

失敗した場合、トークンにそのスペースへのアクセス権がない可能性があります。その旨をユーザーに伝え、選択内容を再確認するか、適切なアクセス権を持つ Contentful アカウントに切り替えるよう依頼してください。

Eval — 続行する前にステップ 1 を確認する
# Use the Node the checker validated (NODE_BIN_DIR from the prereq JSON).
export PATH="<NODE_BIN_DIR>:$PATH"
$PYTHON_CMD --version   # must print Python 3.x
node --version          # must print v20.x or higher
csdx --version          # must print a version number
csdx auth:whoami        # must print a logged-in email
contentful --version    # must print a version number
contentful space list   # must return a list of spaces — not "You have to be logged in"

(export が影響するのはこの Eval ブロックのみです。コマンド間でシェルの状態は保持されないため、後続のステップでも概要の原則に従い、PATH="<NODE_BIN_DIR>:$PATH" を前置する必要があります。)

合格基準:

  • $PYTHON_CMD が検出され、Python 3.x を返して終了コード 0 で終了する
  • Node のメジャーバージョンが 20 以上
  • csdx auth:whoami がメールアドレスを返す("No user logged in" ではない)
  • cs_login.org_uid が null ではない(ステップ 1.2 / 1.3 で前提条件の JSON から取得済み)
  • contentful space list が認証エラーなしで行を返す
  • SPACE_ID が 1.6 でのユーザーの選択から取得されている

注:contentful whoami はすべての CLI バージョンで使用できるわけではありません。代わりに contentful space list を使って認証を確認してください。

すべての確認に合格した場合:

[PROGRESS] 移行の進行状況ブロックを出力します: ステップ 1 → "completed"、ステップ 2 → "in_progress"、ステップ 3–4 → "pending"。

その後、ステップ 2 に進みます。いずれかの確認に失敗した場合は、問題を修正し(該当するサブステップを再実行)、再確認してください。


ステップ 2 — migrate CLI プラグインをインストールする

[PROGRESS] 移行の進行状況ブロックを出力します: ステップ 1 → "completed"、ステップ 2 → "in_progress"、ステップ 3–4 → "pending"。

csdx migrate:* コマンドは @contentstack/cli-external-migrate から提供されます。このブロックを実行すると、自動的にインストールまたは更新されます。変更があった場合、または失敗した場合にのみ出力されるよう、意図的に無出力にしています。

PLUGIN_NAME="@contentstack/cli-external-migrate"

# Version currently installed (empty string if not installed)
INSTALLED=$(csdx plugins 2>/dev/null \
  | grep -F "$PLUGIN_NAME" \
  | grep -oE '[0-9]+\.[0-9]+\.[0-9]+[^ ]*' \
  | head -1)

# Latest version on npm
LATEST=$(npm view "$PLUGIN_NAME" version 2>/dev/null)

if [ -z "$LATEST" ]; then
  echo "PLUGIN_NPM_UNAVAILABLE"
elif [ -z "$INSTALLED" ]; then
  csdx plugins:install "$PLUGIN_NAME" && echo "PLUGIN_INSTALLED:$LATEST" || echo "PLUGIN_INSTALL_FAILED"
elif [ "$INSTALLED" = "$LATEST" ]; then
  echo "PLUGIN_UP_TO_DATE:$INSTALLED"
else
  csdx plugins:uninstall "$PLUGIN_NAME" \
    && csdx plugins:install "$PLUGIN_NAME" \
    && echo "PLUGIN_UPDATED:$INSTALLED→$LATEST" \
    || echo "PLUGIN_UPDATE_FAILED:$INSTALLED→$LATEST"
fi

最終行を次のように解釈します。

  • PLUGIN_UP_TO_DATE:<version> → すでに最新バージョンです。Eval に進みます。(無出力のままにし、ユーザーに「すでに最新です」と報告しないでください。)

  • PLUGIN_INSTALLED:<version> → 新たにインストールされました。簡潔にユーザーに伝えてから、続行します。

  • PLUGIN_UPDATED:<old>→<new> → 古いバージョンをアンインストールし、最新バージョンをインストールしました。プラグインが更新されたことを簡潔にユーザーに伝えてから、続行します。

  • PLUGIN_INSTALL_FAILED / PLUGIN_UPDATE_FAILED:<old>→<new> → インストールまたは再インストールでエラーが発生しました。エラー出力をユーザーに表示してから、次のメッセージを表示して待ちます。

    プラグインの自動インストールに失敗しました。ターミナルで次のコマンドを手動で実行し、完了したら Continue をクリックしてください。

    csdx plugins:install @contentstack/cli-external-migrate
    

    ユーザーが Continue をクリックした後、プラグインが動作していることを確認してから続行するため、下記の Eval に直接進みます。

  • PLUGIN_NPM_UNAVAILABLE → npm view が何も返しませんでした(ネットワークの問題、またはパッケージがまだ公開されていない可能性があります)。次のメッセージをユーザーに表示して待ちます。

    プラグインのバージョンを確認するために npm に接続できませんでした。ターミナルで次のコマンドを手動で実行し、完了したら Continue をクリックしてください。

    csdx plugins:install @contentstack/cli-external-migrate
    

    ユーザーが Continue をクリックした後、下記の Eval に進みます。

評価 — 次に進む前にステップ 2 を検証する
csdx migrate --help

合格基準:

  • 出力に migrate:create(ステップ 3 で使用するワンショットコマンド)が含まれていること

migrate:create が含まれている場合は、移行チェックリストを更新し、cf-step2="completed"、cf-step3="in_progress" に設定してから、ステップ 3 に進みます。コマンドでエラーが発生した場合、または migrate:create が見つからない場合は、上記のインストールブロックを再実行して、もう一度検証してください。


ステップ 3 — コンテンツの移行

[PROGRESS] 移行の進捗ブロックを出力します: ステップ 1~2 → "completed"、ステップ 3 → "in_progress"、ステップ 4 → "pending"。

この単一のコマンドで、Contentful のスペースをエクスポートし、コンテンツを Contentstack バンドルに変換して、新しいスタックにインポートします。マスターロケールは、エクスポートのデフォルトロケールから自動検出されます。

3.0 — 保存済みの Management Token を取得する

Contentful CLI は、ログイン時のトークンを ~/.contentfulrc.json に保存します。migrate:create がプロンプトを表示せずに使用できるよう、今すぐ読み取ります。

$PYTHON_CMD -c "
import json, sys, pathlib
for p in ['~/.contentfulrc.json', '~/.config/contentful/config.json']:
    f = pathlib.Path(p).expanduser()
    if f.exists():
        d = json.loads(f.read_text())
        tok = d.get('managementToken') or d.get('cmaToken') or d.get('management_token')
        if tok:
            print(tok)
            sys.exit(0)
print('NOT_FOUND', file=sys.stderr)
sys.exit(1)
"

出力された値を CONTENTFUL_MANAGEMENT_TOKEN として取得します。ユーザーに表示したり、ファイルに書き込んだりしないでください。

トークンが見つからない場合(終了コード 1)、トークンを解決できなかったことをユーザーに伝えます。再度 contentful login を実行してから、再試行するよう依頼してください。

3.1 — 実行前に確認する

ユーザーに次のように伝えます。

"コンテンツの移行を開始する準備ができました。Contentful のスペースをエクスポートして、コンテンツを変換し、組織内の新しいスタックにインポートします。続行してもよろしいですか?"

コマンドを実行する前に、確認を待ってください。

3.2 — migrate:create を実行する(出力をログファイルに保存)

インポートの logs/ ディレクトリと _backup_*/ が $SESSION_DIR に書き込まれるよう、ここから実行します。これにより、ユーザーのプロジェクトディレクトリをクリーンな状態に保てます。tee を通して出力をリアルタイムで表示すると同時に、解析用に保存します。

cd "$SESSION_DIR" && \
csdx migrate:create --source contentful \
  --space-id "$SPACE_ID" \
  --source-token "$CONTENTFUL_MANAGEMENT_TOKEN" \
  --org "$ORG_UID" \
  --download-assets \
  --output "$SESSION_DIR" \
  --workspace "$SESSION_DIR" \
  -y \
  2>&1 | tee "$SESSION_DIR/migrate-create.log"

フラグに関する注記:

  • --space-id / --source-token — Contentful のソース(トークンは 3.0 で解決済み)
  • --org — ステップ 1 で取得した組織 UID。ここに新しいスタックが作成されます
  • --source contentful — 移行元が Contentful であることを指定します
  • --download-assets — 移行にアセットのバイナリを含めます
  • --output "$SESSION_DIR" — バンドルが $SESSION_DIR/bundle/ に書き込まれます
  • --workspace "$SESSION_DIR" — エクスポート JSON が $SESSION_DIR/export.json に保存されます
  • -y — 内部の確認プロンプトを省略します(上記ですでに確認済みです)
  • cd "$SESSION_DIR" — logs/ と _backup_*/ が CWD ではなくセッションディレクトリに作成されるようにします

このコマンドは 3 つのフェーズを実行し、次のように進行状況を表示します。

フェーズ 1 — エクスポート(取得の進捗を表示し、エンティティ数の表で終了します):

┌────────────────────────┐
│ Exported entities      │
├───────────────────┬────┤
│ Content Types     │ 16 │
│ Entries           │ 53 │
│ Assets            │ 21 │
│ Locales           │ 2  │
│ …                 │ …  │
└───────────────────┴────┘
Stored space data to json file at: <SESSION_DIR>/export.json

フェーズ 2 — 変換(エクスポートを Contentstack バンドルに変換します):

  validate   ✓  export.json
  extract    ✓  2 locales · 16 types
  transform  ✓  53 entries · 13 types  →  <SESSION_DIR>/bundle
  Bundle: <SESSION_DIR>/bundle (16 types, 53 entries)

フェーズ 3 — インポート(スタックを作成してコンテンツをインポートし、サマリーボックスで終了します):

✓ Stack created · via cma
──────────────────────────────────────
  Stack name : Contentful Migration 2026-06-08
  Stack key  : blt3e69b8da307655a7
  Region     : AWS-NA
──────────────────────────────────────
… (import progress) …
SUCCESS: Successfully imported the content to the stack named … with the API key blt… .
SUCCESS: The log has been stored at: <SESSION_DIR>/logs
✓ Bundle metadata written: <SESSION_DIR>/bundle/metadata.json
✓ Migration complete
──────────────────────────────────────
  Stack name : Contentful Migration 2026-06-08
  Stack key  : blt3e69b8da307655a7
  Region     : AWS-NA
──────────────────────────────────────
3.3 — 出力を解析し、セッション変数を抽出する

コマンドの実行が完了したら、実際の出力に一致する正確なパターンで、取得したログを解析します。

$PYTHON_CMD - <<'PYEOF'
import re, pathlib, os

raw = pathlib.Path(os.environ["SESSION_DIR"] + "/migrate-create.log").read_text()
# Strip ANSI escape codes — the import phase wraps paths in color sequences
log = re.sub(r'\x1b\[[0-9;]*m', '', raw)

# Patterns matched against real command output
export_json = re.search(r'Stored space data to json file at:\s+(\S+)',  log)
bundle_dir  = re.search(r'Bundle:\s+(\S+)',                             log)
stack_name  = re.findall(r'Stack name\s*:\s*(.+)',                      log)
stack_key   = re.findall(r'Stack key\s*:\s*(blt\w+)',                   log)
region      = re.findall(r'Region\s*:\s*(\S+)',                         log)
log_dir     = re.search(r'SUCCESS: The log has been stored at:\s+(\S+)',log)
metadata    = re.search(r'Bundle metadata written:\s+(\S+)',            log)

session_dir = os.environ["SESSION_DIR"]
# Use last match for stack fields — the final summary box is authoritative
print("EXPORT_JSON="   + (export_json.group(1).strip() if export_json else session_dir + "/export.json"))
print("BUNDLE_DIR="    + (bundle_dir.group(1).strip()  if bundle_dir  else session_dir + "/bundle"))
print("STACK_NAME="    + (stack_name[-1].strip()       if stack_name  else "UNKNOWN"))
print("STACK_KEY="     + (stack_key[-1].strip()        if stack_key   else "UNKNOWN"))
print("REGION="        + (region[-1].strip()           if region      else "UNKNOWN"))
print("LOG_DIR="       + (log_dir.group(1).strip()     if log_dir     else "UNKNOWN"))
print("METADATA_PATH=" + (metadata.group(1).strip()    if metadata    else session_dir + "/bundle/metadata.json"))
PYEOF

出力された各 KEY=value をセッション変数として取得します。

変数ログ内の正確なソース行使用先
EXPORT_JSONStored space data to json file at: <path>下記の評価
BUNDLE_DIRBundle: <path> (N types, N entries)派生パス
STACK_NAMEStack name : …(最後の出現箇所 — 最終サマリーボックス)ステップ 5 の振り返り
STACK_KEYStack key : blt…(最後の出現箇所 — 最終サマリーボックス)ステップ 5 の振り返り
REGIONRegion : …(最後の出現箇所 — 最終サマリーボックス)ステップ 4 の環境変数
LOG_DIRSUCCESS: The log has been stored at: <path>下記の評価
METADATA_PATH✓ Bundle metadata written: <path>ステップ 4 の認証情報

マッパーのパスも設定します(常に $BUNDLE_DIR/mapper.json)。

MAPPER_PATH="$BUNDLE_DIR/mapper.json"

値が UNKNOWN の場合は、$SESSION_DIR/migrate-create.log を直接確認して該当行を探し、続行する前に手動で設定します。

3.4 — ユーザーに進捗の概要を示す

取得したログから、エンティティ数の表と最終スタックの概要の両方を表示します。

# Entity count table from Phase 1 (export)
grep -A 18 'Exported entities' "$SESSION_DIR/migrate-create.log" | head -20

# Final stack summary box from Phase 3 (last occurrence)
grep -A 4 'Stack name' "$SESSION_DIR/migrate-create.log" | tail -6

通常の文章で次のように報告します。

「コンテンツの移行が完了しました — スタック $STACK_NAME($STACK_KEY、$REGION)の準備ができました。」

実行中にトークンの有効期限が切れた場合

長時間のインポート中に Contentstack OAuth トークンの有効期限が切れることがあります。ログには次のような症状が現れます。

stack creation failed.
  CMA:  401 The provided access token is invalid or expired or revoked

再認証して、ステップ 3.1 から再実行します。

csdx auth:login --oauth

このコマンドは安全に再実行できます。実行するたびに新しいスタックが作成されます。

評価 — 続行前にステップ 3 を確認する

主要な成果物が存在することを確認します。

ls -lh "$METADATA_PATH"
ls -lh "$MAPPER_PATH"

同梱のインポート概要パーサーを実行して、エンティティ数がエクスポート時の数と一致することを確認します。

$PYTHON_CMD "$SKILL_DIR/scripts/parse_import_summary.py" "$LOG_DIR" --export "$EXPORT_JSON"

合格基準:

  • STACK_KEY が blt で始まっている(実際にスタックが作成された)
  • METADATA_PATH と MAPPER_PATH の両方が存在する
  • 取得したログに SUCCESS: Successfully imported… の行が含まれている
  • インポート数がエクスポート数と一致している。一致しない場合は、続行前に差異の理由を説明する必要がある

ユーザーに件数表を提示します。

Module          Imported   (Exported)
Locales         2          2
Content Types   16         16
Assets          21         21
Entries         53         53
✓ Imported into stack "<STACK_NAME>" (<STACK_KEY>)

すべて合格した場合は、移行チェックリストを更新して cf-step3="completed"、cf-step4="in_progress" に設定し、ステップ 4 に進みます。件数に差異がある場合や成果物が見つからない場合は、問題点を明示し、ユーザーに $LOG_DIR を確認するよう案内します。解決するまでは先に進まないでください。

ステップ 4 に引き継ぐセッション変数
変数値用途
BUNDLE_DIRログから取得(または $SESSION_DIR/bundle)インポートバンドルのルート
METADATA_PATH✓ Bundle metadata written: の行から取得スタックの認証情報 — API キー、配信トークン、環境
MAPPER_PATH$BUNDLE_DIR/mapper.jsonContentful → Contentstack のフィールド UID マッピング
STACK_NAME最終サマリーボックスから取得ステップ 5 の振り返り
STACK_KEY最終サマリーボックスから取得ステップ 5 の振り返り
REGION最終サマリーボックスから取得ステップ 4 の .env 設定

ステップ 4 — コード移行

[PROGRESS] 移行の進捗ブロックを出力します。 ステップ 1〜3 → "completed"、ステップ 4 → "in_progress"。

コンテンツを Contentstack に移行できたので、CMS からデータを読み取るアプリケーションコードを移行します。 このステップでは、検出 → 計画 → 書き換え → 評価のサイクルを実行し、移行後に 13 項目をチェックします。

4.1 — 入力の収集

repoPath — 移行するアプリケーションのローカルファイルシステム上のパスです。 ユーザーに尋ねます:

「移行するコードベースのローカルパスを教えてください。(例:/Users/you/projects/my-app)」 リモートの git リポジトリの場合は、まずクローンします:

cd "$SESSION_DIR" && git clone <REPO_URL>

次に、クローンしたディレクトリを repoPath として使用します。

mapperPath — Step 3 で生成されたフィールドマッピング JSON です。存在することを確認します:

ls -lh "$SESSION_DIR/bundle/mapper.json"

ファイルが別のパスにある場合(例:ユーザーがカスタムワークスペースを使用した場合)、正確なパスを確認するようユーザーに尋ねます。

Read ツールで mapperPath を読み込み、各コンテンツタイプから fieldMapping 配列を抽出して、Contentful のフィールド UID → Contentstack のフィールド UID の対応表を作成します。続行する前に、抽出した表をユーザーに提示して確認してもらいます。

metadataPath — Step 3 のインポートで書き込まれた認証情報 JSON です。存在することを確認して読み込みます:

ls -lh "$SESSION_DIR/bundle/metadata.json"

Read ツールで metadataPath を読み込みます。これには新しいスタックの Delivery SDK 認証情報が含まれているため、ユーザーに認証情報を尋ねる必要はありません:

metadata.json キー用途備考
stack_api_keyCS_API_KEYblt… Stack API Key — stack_id ではありません
delivery_tokenCS_DELIVERY_TOKEN公開コンテンツ用 SDK
preview_tokenCS_PREVIEW_TOKENアプリが Live Preview(§18)を使用する場合のみ
environmentCS_ENVIRONMENT例:master
stack_id(スタック UID)識別子のみ — SDK の API キーとして使用しないでください

リージョン(例:AWS-NA)は csdx config:get:region から取得します。トークンはシークレットとして扱い、決してユーザーに表示したり、このワークスペース内のファイルに書き込んだりしないでください。移行したアプリのローカル .env(gitignore 対象にしてください)にのみ設定します。metadata.json がない場合は、CS_API_KEY、CS_DELIVERY_TOKEN、CS_ENVIRONMENT をユーザーに尋ねてください。

4.2 — ログ記録

まず {SKILL_DIR}(運用原則「バンドルされたスクリプトとリファレンス」を参照)を解決します。これはこのスキルがインストールされているディレクトリです。シェル変数に設定して、以下のすべてのコマンドから、あらゆる OS またはアシスタントでバンドルされたスクリプトに移植可能な方法でアクセスできるようにします:

SKILL_DIR="<absolute path to this skill's install dir>"   # the folder holding this SKILL.md

このステップの最初のアクションから、"$SKILL_DIR/scripts/log.sh" を使ってすべてをセッションログに記録します:

  • log.sh <target> user-input "<what the user asked/answered>" — ユーザーからの入力ややり取りをすべて記録します。
  • log.sh <target> decision "<detection results, choices, assumptions, guessed UIDs>"。
  • log.sh <target> ai-action "<files/queries you changed>" および communication "<what you told the user>"。
  • log.sh <target> exception "<any error, blocker, or uncertainty you hit>" — 例外をすべて記録します。
  • 出力と終了コードを取得し、失敗を例外として自動記録できるよう、シェルコマンドはこのスクリプト経由で実行します:log.sh <target> run "typecheck" -- npx tsc --noEmit。

最後に log.sh <target> summary を実行します。ログは <target>/.migration/(session.log + session.jsonl + コマンドごとの出力)に保存されます。"$SKILL_DIR/scripts/README.md" を参照してください。

4.3 — リファレンス(最初に読む)

すべての API、レスポンス形式、クエリーオペレーター、リッチテキストレンダラー、アセット変換、GraphQL クエリー、Live Preview API について、ソースを検証した完全なマッピングは次の場所にあります。

{SKILL_DIR}/references/CONTENTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md(このスキルのインストールディレクトリから Read ツールで読み取ってください。{SKILL_DIR} の運用原則を参照してください)

何よりも先に、全体を読んでください。これが唯一の信頼できる情報源です。従ってください。ドキュメントに具体的な記述がある場合、以前の知識に頼らないでください。主な内容は次のとおりです。

  • §0.1 — データアクセス方式の検出(決定表)
  • §1–§16 — REST Delivery SDK(@contentstack/delivery-sdk)のマッピング
  • §17 — GraphQL Content API の移行
  • §18 — Live Preview/ドラフトモードの移行
  • §19 — 生の REST/fetch とフレームワークのソースプラグイン
  • §13 の注意事項、§15 のチェックリスト
4.4 — 手順
  1. 検出(ドキュメント §0.1)。 アプリの言語、フレームワーク、使用しているデータアクセス方式、および Live Preview/ドラフトモードを実装しているかを特定します。同じ言語とフレームワークを使用し、同じ方式(REST→REST、GraphQL→GraphQL、プレビュー→プレビュー)を維持して移行します。調査結果を報告し、確認のため一時停止してください。

  2. 前提条件。 移行先の Contentstack スタックに、対応するコンテンツモデルと公開済みコンテンツがすでにあることを確認し、手順 4.1 の Contentful のフィールド ID から Contentstack のフィールド UID へのマッピングも確認します。UID が不明な場合は、Contentstack のサンプルエントリーから推定し、推測した箇所すべてにフラグを付けます。環境変数(CS_API_KEY、CS_DELIVERY_TOKEN、CS_ENVIRONMENT、およびリージョン/ブランチ、さらに Live Preview の場合はプレビュートークン)を確認します。これらはユーザーではなく metadataPath(手順 4.1)から取得します(CS_API_KEY には stack_id ではなく stack_api_key を使用してください)。Contentstack にコンテンツモデルまたはエントリーがまだ存在しない場合は、作業を停止してください。コードの移行には、先にコンテンツをインポートする必要があります(手順 1~3)。

  3. 計画。 次の列を含む表を作成します。ファイル:行 → 移行元の呼び出し → Contentstack での対応方法(ドキュメントのセクションを引用)→ フィールド UID の依存関係 → リスクに関する注記。表を提示し、ファイルを編集する前に一時停止してください。

  4. 移行。 検出した各方式に対応するセクションに従って、ドキュメントに沿って移行します。

    • REST Delivery SDK:§1–§16。GraphQL:§17。生の REST/フレームワークプラグイン:§19。
    • リッチテキスト/アセット/ロケール/ページネーション:§9–§12。 周辺のコードスタイルとフレームワーク既存のデータ取得方式に合わせて、最小限かつ機械的な変更を行います。関数/コンポーネントの契約を維持してください。
  5. LIVE PREVIEW。 手順 1 でプレビュー/ドラフトモードが見つかった場合は、ソースと同じ範囲(ルート、コンポーネント、SSR とクライアント側のどちらか、クリックして編集する方式か読み取り専用か)に合わせて、§18 に従って再実装します。プレビュートークンはサーバー側に保持し、既存のプレビューのゲーティング/ルーティングを維持してください。

  6. 検証 — eval スイートを実行してください(必須であり、省略できません)。 まず依存関係をインストールしてビルド eval に意味のある結果が出るようにし、その後、同梱の eval を並列で実行します。

    bash "$SKILL_DIR/scripts/run-all.sh" <path-to-migrated-app>
    

    最大限に並列化するには、"$SKILL_DIR/scripts/"NN\_\*.sh ごとに 1 つのエージェントを起動してもかまいません。各チェックで検出される内容と終了コードの意味については、"$SKILL_DIR/scripts/README.md" を参照してください。

    • ハードゲートの FAIL/ERROR(残存コード、フィールドアクセス、SDK 初期化、ビルド、シークレット) ⇒ 移行は未完了です。 合格するまで修正して再実行してください。
    • file:line に示された review-eval の指摘をすべてトリアージしてください。静的 grep は不具合を確定するものではなく、疑わしい箇所を示すものです。真の問題は修正し、残る指摘については安全である理由を明示してください。指摘を説明せずに退けてはいけません。
    • ビルドの成功は必要ですが、それだけでは不十分です。参照配列の不具合、誤ったフィールド UID、RTE の出力は検出できません。実際の Contentstack スタックに対するライブクエリーもスモークテストしてください。
    • ドキュメントに「最新のドキュメントで確認」とある項目(GraphQL のホスト/ヘッダー、Live Preview のフロントエンド API)は、推測せず、確定前に確認してください。
4.5 — ガードレール
  • コンテンツモデリングを変更したり、エントリーを移動/インポートしたりしないでください。このステップの対象外です。
  • すべての参照のデリファレンスを安全な配列アクセス(?.[0])に変換し、null 安全性を監査してください (Contentstack は参照を単一のオブジェクトではなく配列として解決します)。
  • UID や動作が不確かな場合は、// TODO(migration): コメントを残して一覧にしてください。推測を黙って行わないでください。
4.6 — 仕上げ

[PROGRESS] 移行の進捗ブロックを出力します: ステップ 1~4 はすべて "completed" にします。その後、ただちにステップ 5 に進みます。

移行全体の最終概要をユーザーに提示します:

  • コンテンツ: スタックにインポートした件数(ステップ 3 の情報)。
  • コード: 変更したファイル、選択したリッチテキストのレンダリング方式、確認が必要な推測した UID、対応が必要な TODO(migration) の呼び出し箇所。
  • 評価結果: すべてのハードゲートに合格したか、トリアージが必要なレビュー指摘事項。
  • 検出の概要: 検出した言語/フレームワーク/アプローチ、Live Preview が存在したかとその再実装方法、推測した UID のすべて、すべての TODO、完全には移行されていない呼び出し箇所(geo クエリ、locale: '*'、スペースをまたぐ ResourceLinks、同等の機能がない GraphQL 機能)。
  • 次のステップ: metadata.json の値を使って、アプリのローカル .env に CS_* 環境変数を設定します(CS_API_KEY = stack_api_key、CS_DELIVERY_TOKEN = delivery_token、CS_ENVIRONMENT = environment、Live Preview を使用する場合は CS_PREVIEW_TOKEN = preview_token、さらに csdx config:get:region で取得したリージョンを設定します)。アプリを Contentstack に接続して実行し、ページがレンダリングされることを確認します。

ステップ 5 — Contentstack へようこそ 🎉

移行が完了しました。以下のメッセージをユーザーに表示します。セクション見出し、アイコン、改行を保持し、表示どおりにレンダリングしてください:


Contentstack

╔══════════════════════════════════════════════════════════════════╗
║                                                                  ║
║   🚀  Welcome to Contentstack!                                   ║
║                                                                  ║
║   The Agentic Experience Platform                                ║
║                                                                  ║
╚══════════════════════════════════════════════════════════════════╝

「世界最高のデジタルエクスペリエンスは、Contentstack 上で実現されています。」

🎊 移行が完了しました。

コンテンツ、コンテンツタイプ、アセット、ウェブサイトのコードはすべて、Contentful から Contentstack に正常に移行されました。今回の作業内容を簡単に振り返ります:

✅移行した内容
📦コンテンツタイプとフィールド定義
🖼️アセットとメディア
📝エントリーとローカライズされたコンテンツ
💻ウェブサイトのデータレイヤーコード
📂 移行したファイルの場所

このセッションで取得した正確なパスを使って、次の表を完成させて表示します:

成果物場所
🗂️ Contentful のエクスポート$SESSION_DIR 内の export.json のパス(ステップ 3)
📦 Contentstack のインポートバンドル$SESSION_DIR/bundle/(ステップ 3)
🔑 スタックの認証情報$SESSION_DIR/bundle/metadata.json(ステップ 3)
📋 インポートログmigrate:create が出力したログディレクトリ(ステップ 3)
💻 移行したウェブサイトのコードステップ 4 でユーザーが指定した repoPath
🔍 コード移行ログ<repoPath>/.migration/session.log

デフォルトのパスや推測したパスは使用しないでください。各ステップのコマンド出力から取得した実際のパスのみを使用してください。


🌟 世界のトップブランドに信頼されるプラットフォームをご利用いただけます

Contentstack は、小売、メディア、金融など、さまざまな業界の企業向けにデジタルエクスペリエンスを支えています。あらゆるチャネルで妥協なく、大規模にコンテンツを配信できます。

ご利用いただける機能:

機能お客様にとってのメリット
⚡ コンポーザブルアーキテクチャ最適なツールを自由に組み合わせて、お客様独自のスタックを構築できます
🌍 マルチリージョン配信世界中のどこでも、コンテンツをすばやく配信できます
🔄 オムニチャネルパブリッシングウェブ、モバイル、IoT、音声など、すべてをひとつのコンテンツハブで管理できます
🛡️ エンタープライズレベルのセキュリティSOC 2 Type II、GDPR、HIPAA に対応
🤝 専任サポートドキュメントだけでなく、導入、移行などを担当するスタッフがサポートします
🧩 マーケットプレイスとインテグレーション100 以上の既成コネクターで、現在お使いのツールを接続できます

🚦 おすすめの次のステップ
  1. ライブサイトを確認する — 新しい CS_* 環境変数を使ってアプリを起動し、Contentstack に接続してページが表示されることを確認します。
  2. Contentstack ダッシュボードを活用する — チームを招待し、ロールを設定して、公開ワークフローを構成します。
  3. Webhook と自動化を設定する — 公開イベントをきっかけに、デプロイや Slack アラート、カスタムワークフローを実行します。
  4. Contentstack のドキュメントを確認する — contentstack.com/docs は、Delivery SDK、GraphQL、Live Preview のガイドを確認できる場所です。
  5. アカウントチームに連絡する — 運用を開始したことを伝えましょう。追加機能を利用できるようにしたり、高度な機能について案内したりしてもらえます。

💡 ヒント: Contentstack Developer Hub をブックマークしておきましょう。SDK リファレンス、スターターアプリ、動画チュートリアルがあり、新しいプラットフォームを最大限に活用するのに役立ちます。


移行の完了、おめでとうございます。
最適な選択をされました。世界最高のデジタル体験は Contentstack 上で構築されています。これからは、あなたのデジタル体験もそうなります。
🚀

コメント