newmo 技術ブログ

技術で地域をカラフルに

PayloadCMSを「管理画面基盤」として使う — 半年の学びと設計判断

この記事は newmo Advent Calendar 2025 15日目の記事です。

はじめに

newmoでは2025年6月から求人サービス「newジョブ」の開発を進めており、その中でHeadless CMSとしてPayloadCMSを採用しました。

本記事では、約半年の本番運用を通じて得た知見を共有します。

newjob.jp
newジョブ サイト・トップ

なぜHeadless CMSが必要だったか

jinzaiチームは少人数で構成されており、以下のような複数の用途に対応する必要がありました。

  • 求人情報の管理・配信
  • SEO対策のための構造化データ管理
  • 運用チームによるコンテンツ編集
  • 将来的な機能拡張への対応

既存のSaaSでは要件を満たしきれず、カスタマイズ性の高いHeadless CMSが必要でした。


PayloadCMSとは

PayloadCMSは、TypeScriptで書かれたオープンソースのHeadless CMSです。

payloadcms-1
payloadcms-2

https://payloadcms.com/use-cases/headless-cms

主な特徴

特徴 説明
TypeScript Native スキーマ定義から型が自動生成される
Next.js統合 v3.0からApp Routerにネイティブ対応
セルフホスト可能 自社インフラでの運用が可能
柔軟なDB対応 PostgreSQL、MongoDB、SQLiteに対応
コード駆動 GUIではなくコードでスキーマを定義

本比較はnewジョブの要件に基づく用途適合の観点です。他用途では各CMSの強みが発揮され得ます。

Headless CMSの中での位置づけ

PayloadCMSは「コードでCMSを定義する」アプローチを取っており、開発者にとっての自由度が高いのが特徴です。

2024-2025年の動向

  • 2024年11月: PayloadCMS 3.0リリース - Next.js App Router統合
  • 2025年6月: Figmaによる買収発表
  • 現在: v3.64.0が最新(2025年12月時点)

買収後もオープンソースとして開発が継続されており、Figmaのデザインツールとの連携強化が期待されています。

参考:


PayloadCMS選定の理由

複数のHeadless CMSを比較検討した結果、PayloadCMSを選定しました。

比較検討したCMS

CMS 特徴 今回の用途における結論
PayloadCMS TypeScript native、Next.js統合 今回の用途で最適と判断(型安全性と拡張性)
Strapi プラグインエコシステム充実 今回の要件に対する適合度は相対的に低い
Sanity リアルタイム編集、GROQ 学習コストが今回の条件では高めと判断
microCMS 日本製、シンプル 将来の拡張要件への適合度が相対的に低い

本比較はnewジョブの要件に基づく用途適合の観点です。他用途では各CMSの強みが発揮され得ます。

決め手となったポイント

1. TypeScript Native

スキーマ定義からフロントエンドまで一貫した型安全性を確保できます。コレクション定義がそのまま型定義になるため、型定義の二重管理が不要です。

2. Next.js App Router統合

PayloadCMS 3.0からNext.js App Routerにネイティブ対応しています。CMSがNext.jsアプリとして動作するため、カスタムページの追加が容易です。

3. 拡張性

CMSを越えて「管理画面基盤」として使える拡張性が決め手に。hookによる処理拡張や管理画面のカスタムが素直に実装でき、実運用で効きました。特に次の2点は生産性と将来の対応力に直結します。

  • REST/GraphQL APIの自動生成: スキーマ変更が型とAPIに自動伝播し、SSGビルドや外部連携の変更コストを抑制
  • 認証機能の提供(管理画面とAPI): ロールベースの認可やAPIキー運用を内製せずに利用でき、セキュリティと開発速度を両立

実際に各CMSをinitializeして触った結果、PayloadCMSが最も柔軟に要件をカバーできると判断しました。


Figma買収とセルフホスト移行

予期せぬ方針転換

元々、運用負荷を下げるためPayload Cloud(PayloadCMS公式のホスティングサービス)を利用する予定でした。

しかし、リリース直前の2025年6月、FigmaによるPayloadCMS買収が発表され、Payload Cloudが一時的にクローズされることになりました。

参考: Payload is joining Figma!

Cloud Runへの移行

急遽、Google CloudのCloud Runにセルフホストする方針に転換しました。

  • 移行作業期間: 約1週間
  • タイミング: リリース前だったことが幸い
  • 結果: 問題なく稼働開始

「どちらでも動く」という安心感

この経験から得られた重要な学びは、PayloadCMSが「どちらでも動く」設計になっているということです。

// payload.config.ts - 本番環境での設定
db: postgresAdapter({
  pool: {
    connectionString: env.DATABASE_URI,
  },
  push: false, // セルフホスト時は自動マイグレーション無効化
}),

Payload CloudでもセルフホストでもPayloadCMS自体のコードは同じため、ホスティング先の変更に柔軟に対応できました。


monorepo統合の実際

newmoではpnpm workspaceを使った大規模monorepoでサービスを開発しています。PayloadCMSプロジェクトもこのmonorepoに統合しました。

共有できたもの

1. DBマイグレーション(Atlas)

newmoではAtlasを使ってデータベーススキーマを管理しています。PayloadCMSのスキーマもこの仕組みに統合しました。

# PayloadCMSの設定からSQLスキーマを抽出
pnpm run --filter @newmo-app/payloadcms extract-schema

# 出力先: server/component/newjob/db/postgres/schema.sql

extract-schemaコマンドでは、以下の処理を行っています。

// 入力: コレクション定義
// collections/Tags.ts
export const Tags: CollectionConfig = {
  slug: "tags",
  fields: [
    { name: "name", type: "text", required: true },
    { name: "order", type: "number", defaultValue: 0 },
    { name: "isPublished", type: "checkbox", defaultValue: true },
    {
      name: "color",
      type: "select",
      options: [
        { label: "赤", value: "red" },
        { label: "青", value: "blue" },
        { label: "緑", value: "green" },
      ],
    },
  ],
};
// scripts/extract-payload-schema.ts(疑似コード)
async function main() {
  // 1. PayloadCMSのDrizzleスキーマを生成
  await exec("npx payload generate:db-schema");

  // 2. Drizzle KitでSQLを生成
  await exec("npx drizzle-kit generate");

  // 3. Atlasが管理するディレクトリに出力
  await consolidateSQL("server/component/newjob/db/postgres/schema.sql");
}
// 出力: schema.sql
CREATE TYPE "public"."enum_tags_color" AS ENUM('red', 'blue', 'green');

CREATE TABLE "tags" (
  "id" serial PRIMARY KEY NOT NULL,
  "name" varchar NOT NULL,
  "order" numeric DEFAULT '0',
  "is_published" boolean DEFAULT true,
  "color" "enum_tags_color",
  "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
  "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);

これにより、PayloadCMSのコレクション定義を変更するだけで、Atlasのマイグレーションフローに自動的に乗せることができます。

参考: Atlas - Database Schema as Code

2. デザインシステム

monorepo内の共通デザインシステム(Panda CSS)をそのまま利用できます。

// payload-cms/panda.config.ts
import { defineConfig } from "@pandacss/dev";
import { newmoPreset } from "@newmo-app/panda-preset";

export default defineConfig({
  presets: [newmoPreset],
  // ...
});

3. CI設定

lint、test、型チェックなどの設定をmonorepo全体で共有しています。

# .github/workflows/ci.yml(抜粋)
- name: Lint
  run: pnpm run lint

- name: Type Check
  run: pnpm run typecheck

新しくPayloadCMSプロジェクトを追加しても、細かいCI設定を一から書く必要がありません。

参考: Renovate + pnpm catalog でmonorepoの依存関係を効率管理

4. デプロイの独立性

社内の共有インフラに乗りながらも、アプリケーションのデプロイは独立して行えます。PayloadCMSのapp単体でデプロイ可能で、他のアプリに影響を与えません。


コレクション設計の学び

PayloadCMSを使い始めて最初に躓いたのが、コレクション(画面)とDBテーブル(ドメイン)の設計の分離でした。

コレクション ≠ DBテーブル

PayloadCMSのコレクションは管理画面と密接に結びついています。一方、ドメイン設計の観点からはDBテーブルを正規化したい場面があります。

DB設計時に考えること

設計方針:拡張にオープン、画面は分離

私たちが採用した方針は以下の通りです。

  1. ドメイン設計を優先してDBテーブル(コレクション)を設計
  2. 管理画面の利便性は別途カスタマイズで対応
  3. 拡張性を確保するため、将来の要件も見据えた構造に

hookの活用例

例えば、求人の住所情報が更新されたら自動的に緯度経度を取得するhookを実装しています。

// collections/JobListings.ts(疑似コード)
const JobListings: CollectionConfig = {
  slug: "job-listings",
  hooks: {
    beforeChange: [
      async ({ data, req }) => {
        // 住所IDから位置情報を同期
        if (data.addressId) {
          const address = await req.payload.findByID({
            collection: "job-listings-addresses",
            id: data.addressId,
          });

          if (address) {
            data.location = {
              type: "Point",
              coordinates: [address.longitude, address.latitude], // 緯度経度情報
            };
          }
        }
        return data;
      },
    ],
  },
  // ...
};

このように、コレクション間の連携もhookで柔軟に実現できます。

参考: PayloadCMS Hooks

余談ですが、DBにPostgreSQLを採用している場合に上記のPoint型を利用すると、自動で利用できるAPIへのリクエストでそのまま近傍・範囲・インターセクト検索クエリが使える為、位置情報を扱うユースケースにおいてとても便利に利用することが出来ます。

参考: Point Field | Documentation | Payload


まとめ

半年運用しての所感

PayloadCMSを半年間本番運用してきて、特に困った点はありませんでした。

  • v3からのスタート: 最初からv3.0を使っているため、v2からの移行問題なし
  • アップデート追従: 定期的なアップデートも問題なく適用
  • 安定性: 本番環境で大きな障害なし

PayloadCMSを「CMS」ではなく「管理画面基盤」として捉える

PayloadCMSの真価は、単なるCMSではなく、Next.jsベースの管理画面基盤として活用できる点にあると考えています。

  • コンテンツ管理以外の管理機能も追加可能
  • カスタムページ、カスタムAPIの追加が容易
  • 認証・認可の仕組みをそのまま流用

少人数チームで複数の用途に対応する必要がある場合、PayloadCMSは有力な選択肢です。

最後に

Payload を選んだ理由をひと言でまとめるなら、「変更に強いから」。要件が動く前提で、型、API、UI を同時に追従させられる。少人数の開発チームにとってこの“追従の速さ”は安全装置でした。

これからも「速く、雑に」ではなく「速く、正しく」を実現していきたいと思います。

newmo Engineering Advent Calendar 2025: written by Claude Code(opus 4.5) w/ @yui_tang


参考リンク