.../articles/
Firebase から Vercel へ、Contentful から microCMS へ ── Claude Code と進めた移行の記録

Firebase から Vercel へ、Contentful から microCMS へ ── Claude Code と進めた移行の記録

2026.08.05

コーポレートサイトのホスティングを Firebase から Vercel へ、CMS を Contentful から microCMS へ移行し、あわせてサイト全体を日英対応にしました。作業は Claude Code と進めています。

移行そのものより、実データに当ててみて初めて分かった制約のほうが役に立つと思うので、詰まった点を中心に書きます。

移行前後の構成

Before

After

ホスティング

Firebase Hosting → Cloud Functions v2 に全リクエストを rewrite

Vercel Functions(hnd1)

CMS

Contentful(GraphQL / 本文は Markdown)

microCMS(REST / 本文はリッチエディタ)

言語

日本語のみ

日本語 + 英語(/en 配下)

デプロイ

GitHub Actions から firebase deploy

Vercel の Git 連携

Vercel 移行:独自パイプラインが丸ごと消えた

移行前は Remix のビルド成果物を手で移動していました。build/client/assetspublic/ にコピーし、build/serverfunctions/ にコピーしてから Cloud Functions としてデプロイする、という構成です。functions/ には本体と別の package.json があり、依存を二重管理していました。

@vercel/remix の Vite preset を入れたら、この一式が不要になりました。差分は 11,664 行削除・706 行追加。ビルドスクリプトも sass:build && remix vite:build だけになっています。

環境変数をビルド時からランタイムへ

それまで loader の中で import.meta.env.VITE_CONTENTFUL_* を読んでいました。Vite の VITE_ プレフィックスはビルド時にバンドルへ埋め込まれる方式なので、環境変数を変えるたびに再ビルドが必要になります。クライアント側から参照すればそのまま公開されてしまう点も気持ちが悪い。

これを process.env のランタイム参照に変え、あわせて各ページに散らばっていた「Space ID とトークンを引数で渡す」重複をクライアントモジュール側に集約しました。

詰まった点:サーバー専用モジュールがクライアントに混入する

日英でページ実装を共有するため、ページコンポーネントを app/features/ に切り出しました。このとき loader とコンポーネントを同じファイルに置いたらビルドが落ちます。

[commonjs--resolver] Server-only module referenced by client
    '~/client.server' imported by 'app/features/article-list.tsx'

Remix が loader を取り除いてくれるのはルートモジュールだけで、そこから import した普通のモジュールは対象外です。loader を *.server.ts に分離し、ルートファイルからそれぞれを import する形にして解決しました。

Contentful → microCMS:API 設計と実データの検証

Hobby プランの API 数 5 に収める

Contentful には blogPost / work / person / tag / category の 5 種類がありました。そのまま移すと上限ちょうどで余白がありません。

そこでコードを調べたところ、tag(43 種・98 付与)と category はサイト上で一度も使われていませんでした。tag はデータを失わないよう記事のテキストフィールドにカンマ区切りで持たせ、category は移行対象から外しました。結果 3 つに収まり、2 枠を残せています。

contentId に slug をそのまま使う

microCMS のリスト型コンテンツは contentId を任意の文字列で指定できます。Contentful の slug をそのまま使えば /articles/:slug の URL が変わりません。

ただし contentId は英小文字・数字・ハイフン・アンダースコアのみです。100 件中 1 件だけ大文字を含む slug があり、そこは小文字化して loader で 301 リダイレクトを入れました。

公開日は専用フィールドを作らなくてよかった

当初は publishDate フィールドを作るつもりでしたが、microCMS のシステムフィールド publishedAt は書き込み API から直接指定できます。Contentful の公開日をそのまま移せたので、フィールドを 1 つ節約できました。

いっぽう createdAt は書き込めません('createdAt' is unexpected key.)。管理画面の作成日は移行実施日のままになりますが、表示・並び順に使うのは publishedAt なので実害はありませんでした。

リッチエディタ化は「壊れ方を先に測る」

Contentful の本文は Markdown 文字列で、サイト側が marked で HTML 化していました。microCMS ではリッチエディタ(richEditorV2)に変えたかったのですが、リッチエディタは許可した要素以外を保存時に落とします

そこで移行前に、全 100 件の Markdown を marked で変換し、許可リストに無い要素が残るコンテンツを機械的に洗い出しました。

  • コードブロック 187 個・インラインコード 281 個 → codeBlockcode を許可すれば保持できる
  • 見出しは h1〜h4 を使用 → headerOneheaderFive でカバー、h6 が 1 件だけ範囲外
  • コードフェンスの外に生 HTML を含む記事が 5 件 → 編集時に崩れる可能性があるため、別途手直しする課題として起票

「たぶん大丈夫」で進めずに、対象を 5 件に特定できたのが大きかったです。

画像 107 点と 5MB の壁

Contentful のアセットは 312 点ありましたが、実際に参照されているのは 107 点でした。本文中に埋め込まれた画像 URL も収集して移行対象にしています。

途中で 1 件だけ request body too large で失敗しました。microCMS のメディアアップロードは 1 ファイル 5MB までで、5.4MB の写真が引っかかったためです。上限を超えるものだけ Contentful の画像 API(?w=2400&q=80)で縮小してから取得するようにして通しました。

詰まった点:PUT は新規作成専用

移行スクリプトは何度でも安全に再実行できるようにしたかったので PUT /api/v1/{endpoint}/{contentId} を使っていました。ところが 2 回目でこうなります。

400 {"message":"Content is already exists. If you want update, please use PATCH request."}

microCMS の PUT は新規作成専用でした。既存なら PATCH にフォールバックする実装に変えて、冪等性を確保しています。

日英対応

英語ページは /en 配下に置き、root の loader が URL からロケールを判定して全ページへ配る形にしました。各コンポーネントは useLocale() / useT() で参照するだけです。

文言は共通辞書とページ別コピーに分けています。ナビ・カード・フォームなどページをまたぐものは共通辞書へ、1 ページでしか使わない本文はページごとのモジュールへ。こうしないとページが増えるたびに共通辞書が肥大化します。

canonical は片方に寄せない

日英を別 URL で持つとき、canonical を片方に寄せてはいけません。同一内容の重複ではなく別言語の対応版なので、寄せられた側がインデックスから外れて英語圏からの流入を捨てることになります。

正しくは、各言語版が自分自身を canonical に指し、両者の関係を hreflang で示す形です。

<link rel="canonical" href="https://gift-tech.co.jp/en/about" />
<link rel="alternate" hrefLang="ja" href="https://gift-tech.co.jp/about" />
<link rel="alternate" hrefLang="en" href="https://gift-tech.co.jp/en/about" />
<link rel="alternate" hrefLang="x-default" href="https://gift-tech.co.jp/about" />

本文 27 万文字の英訳

記事 78 件・実績 22 件の本文は 27 万文字ありました。これは Claude のサブエージェントを 12 並列で走らせて英訳しています。

翻訳より大事なのは壊れていないことの確認で、生成後に全 100 ファイルを機械検証しました。コードフェンス数の一致(改変ゼロ)、本文の欠落なし、差し替わった URL 14 件を実際に HTTP チェックしてリンク切れゼロ。人名の英語表記が 1 箇所ゆれていたのもこの検証で見つかりました。

それでも機械翻訳なので、公開前の人によるレビューは必要という前提は変えていません。

やってみて

今回いちばん効いたのは、コードを書くことより検証を仕組みにしたことでした。

  • 移行スクリプトを冪等にする(途中で失敗しても再実行できる)
  • 実データを全件流して、壊れ方を件数で把握してから決める
  • 「英語ページに日本語文字が 1 文字も残っていないこと」のように、判定を機械で書く

この 3 つがあると、AI に任せる範囲を広げても結果を確認できます。逆にこれが無いと、動いているように見えて実は壊れている状態に気づけません。実際、記事一覧に見慣れない ID が出て焦った場面がありましたが、検証スクリプトを書いたら本文中の外部リンクの誤検出だと 1 分で判明しました。

written by

.../article/

Articles

すべての記事

Firebase から Vercel へ、Contentful から microCMS へ ── Claude Code と進めた移行の記録

Firebase から Vercel へ、Contentful から microCMS へ ── Claude Code と進めた移行の記録

コーポレートサイトのホスティングと CMS を移行し、あわせて日英対応にしました。移行そのものより、実データに当てて初めて分かった制約のほうが役に立つと思うので、詰まった点を中心にまとめます。

Firebase Functions×Remix で POST データが読めない!?

Firebase Functions×Remix で POST データが読めない!?

Firebase Functions 環境で Remix の action から POST データを読む方法まとめ

経営層・リーダーのための生成AI活用 〜自走型DXのためのアプローチ〜

経営層・リーダーのための生成AI活用 〜自走型DXのためのアプローチ〜

生成AIを活用して、経営者やリーダー自身が課題抽出・打ち手検討を行える体制を構築する。 弊社が提供する“伴走型支援”と組み合わせることで、DX推進の質とスピードを飛躍的に高める方法をご紹介します。

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

monorepo管理しているNext.jsをAmplifyにデプロイしようとした際にいくつか躓く内容があったのでまとめておきます。

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

コロナ禍の影響により、リモートワークの導入をおこなっている制作会社も多く、実際に弊社でも導入しています。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトのデザインは、「この形式」という決まりはありません。ECサイトで売り上げを上げるなら、しっかりとしたコンセプトと、コンセプトを決定するまでのリサーチが必要です。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

新型コロナウイルス感染拡大の影響で、リモートワークが主流になり、弊社でも週のほとんどは各自宅で作業をしています。

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaを利用するWebデザイナーの助けになれると嬉しいです。使い方は本記事をご覧ください。

ECの構築方法、おすすめのECサービス。

ECの構築方法、おすすめのECサービス。

ファッションや家電、スーパーの買い物でさえもECサイトを利用することが当たり前になりました。加えて新型コロナウイルスの影響もあり、弊社にも「どんなプラットフォームを利用したら良いか」「どれくらいコストがかかるのか」などECに関するさまざまなご相談を頂きます。

FastAPIのスキーマクラスをOpenAPIから生成する方法

FastAPIのスキーマクラスをOpenAPIから生成する方法

PythonでAPIを構築する要件があり、フレームワークに比較的モダンなFastAPIを採用しました。FastAPIはバックエンドの開発を行えば自動でOepnApi定義を生成する機能が備わっていますが、今回はこれを使わず、事前に用意したOepnApi定義からFastAPIで利用するスキーマクラスを生成する方法を紹介します。

すべての記事

お問い合わせ