前回、コーポレートサイトの技術選定について書きました。基準はひとつ、「Gitでバージョン管理ができるか」。それだけです、と。
その基準に従ってPayload CMSを選び、MongoDB前提のテンプレートとPostgreSQLの相性問題にもぶつかり、Gitで管理し続けられる形に切り替えました。ここまでが前回の話です。
ただ、選定基準をクリアして、テンプレートの相性問題も乗り越えて、開発環境で動くところまで持っていっても、まだ終わりではありませんでした。今回は、その先――本番公開の段階で踏んだ壁について書きます。
開発環境では、動いていました
受注管理システムのときは、ここでつまずきませんでした。shadcn/ui・Kiranismのテンプレートを応用したときは、Vercelへのデプロイはスムーズでした。ローカルで動いていたものが、そのまま本番でも動きました。
だから今回も、同じように進むと思っていました。
コーポレートサイトの開発環境では、実際、すべてがうまく動いていました。管理画面から画像をアップロードし、Worksページにカバー画像を設定し、各ページにヒーロー画像を配置する。ローカルで確認するかぎり、何の問題もありませんでした。
そのままVercelにデプロイしました。
本番で、画像が消えました
公開されたサイトを開くと、画像が表示されていませんでした。
Worksページのカバー画像、各ページのヒーロー画像、本文中に埋め込んだ画像、OGP用のメタ画像――管理画面からアップロードしたものが、軒並み表示されなくなっていました。
一方で、ロゴやfaviconは無事でした。これらはPayloadの管理画面を経由せず、最初からpublic/ディレクトリ直下に静的ファイルとしてコミットしていたものです。
つまり、壊れていたのはPayloadの管理画面からアップロードした画像だけで、Gitに直接コミットした静的アセットは無事だった。この違いが、原因を探る手がかりになりました。
原因は、.gitignoreでした
調べてみると、原因はシンプルでした。プロジェクトの.gitignoreに、public/media/――つまりPayloadの管理画面からアップロードした画像の保存先――を除外する設定が、最初から入っていたのです。
開発環境では、アップロードした画像がローカルのディスクにそのまま残るので、何の問題もなく表示されます。しかし本番環境では、Gitにコミットされていないファイルはデプロイに含まれません。開発環境で「動いている」ように見えたのは、たまたまローカルにファイルが残っていただけで、実際にはその画像がGitの管理下に一度も入っていなかったのです。
さらに調べると、もうひとつ理由がありました。Vercelのサーバーレス環境は、ファイルシステムが実行のたびに使い捨てられる仕組みになっています。仮に.gitignoreの除外設定がなかったとしても、実行時に管理画面から新しくアップロードした画像は、そのデプロイの中でしか存在せず、次のデプロイやリクエストでは消えてしまいます。
これは、MongoDBとPostgreSQLの相性問題のような、今回固有の事情ではありませんでした。Vercelのようなサーバーレス環境でPayload CMSを動かす場合、共通して踏むことになる構造的な壁です。ローカルディスクへの保存を前提にしたCMSと、実行のたびにファイルシステムが消えるサーバーレス環境は、そもそも相性が良くありません。
@payloadcms/storage-vercel-blobを導入しました
対処として、Payload公式のVercel Blobストレージプラグイン、@payloadcms/storage-vercel-blobを導入しました。管理画面からのアップロード先を、ローカルディスクからVercel Blob(クラウドストレージ)に切り替えるプラグインです。
導入自体は、プラグインをインストールしてMediaコレクションに紐付けるだけで、大きな変更ではありませんでした。ただし、それだけでは終わりませんでした。
既存の画像は、置いてけぼりになります
プラグインを導入しても、それは「これから」のアップロード先を変えるだけです。すでにアップロード済みだった64ファイルは、相変わらずGitにコミットされていない状態のまま残ります。
このため、一時的な移行スクリプトを書きました。ローカルに残っている既存の画像を1件ずつ読み込み、Vercel Blobにアップロードし直すスクリプトです。実行した結果、64件すべてを移行できたことを確認し、このスクリプトは役目を終えたので削除しました。
移行後は、実際にサイトの全ページを確認し、画像が正しく表示されることを確かめました。
トークンの取得で、もう一段つまずきました
Vercel Blobを使うには、BLOB_READ_WRITE_TOKENという環境変数が必要です。これも、Vercelの環境変数を手元に取得するいつものコマンド、vercel env pullで済むと思っていました。
ところが、このトークンだけは取得できませんでした。Vercelのダッシュボード上で「Sensitive」(機密)指定になっている環境変数は、CLI経由では値そのものが返ってこず、プレースホルダーしか書き込まれない仕様だったのです。別のコマンドも試しましたが、同様に弾かれました。
結局、Vercelのダッシュボードを開き、Blobストアの画面から値を直接コピーして、ローカルの環境変数ファイルに手動で貼り付けるという、地道な方法で解決しました。
動くことと、公開できることは、別問題です
今回のことで、はっきりわかったことがあります。ローカルで動作確認が取れていても、それは「公開できる」ことをまったく保証しません。
前回書いたテンプレートの相性問題は、Payload CMSとPostgreSQLという組み合わせ固有の話でした。しかし今回の話は違います。Payload CMSをVercelのようなサーバーレス環境で運用しようとする人なら、データベースの種類に関係なく、ほぼ確実に同じ壁にぶつかります。
.gitignoreでアップロード先を誤って除外していないか。ローカルディスクへの保存を前提にしたままデプロイしていないか。機密指定の環境変数を、CLIで取得できると思い込んでいないか。
これから同じ道を歩む人が、同じ沼で足止めを食わないように。今回の記事が、その回避ポイントとして役に立てば、書いた甲斐があると思っています。
それでも、Payload CMSを選び続ける理由
正直なところ、今回のような壁は、他のCMSであればそもそも踏まなかったかもしれません。ノーコードのCMSであれば、ストレージの設定などユーザーが意識する必要もなく、最初から解決済みだったはずです。
それでも、Payload CMSを使い続けようと思っています。
多くのCMSは、後から機能を拡張しようとするとプラグインを追加する形になります。プラグインは便利な反面、品質のばらつきや、プラグイン同士の競合、アップデートのたびに壊れるリスクを抱え続けることになり、拡張すればするほど管理が煩雑になっていきます。
Payload CMSは違います。コレクションの定義もスキーマもGlobalsの設定も、すべてコードとして残ります。機能拡張は「プラグインを探して入れる」のではなく「コードを書き足す」形で行われるため、変更はGitの差分としてそのまま追跡できます。何が、いつ、なぜ変わったのかが、常にレビュー可能な状態にある。今回のようなストレージ設定のつまずきも、原因を.gitignoreの1行とコードの中に、Git履歴として遡って特定できました。プラグイン地獄の中で同じ調査をしようとしたら、もっと時間がかかっていたはずです。
日本ではまだ採用事例の少ないPayload CMSですが、今回のように「動くこと」と「公開できること」のあいだにある壁を越えてでも使う価値があると、自分は考えています。今後、同じような判断をする人が増えていくはずです。だからこそ、今回踏んだ壁を、先に記録として残しておきたいと思いました。
実際に構築したコーポレートサイトは、以下で公開しています。
https://www.kuros-works.com/
実際に構築したコーポレートサイトのソースコードは、GitHubで公開しています。
https://github.com/kuros-works/kuros-corporate-site