AI

Headless CMS OSS比較:Strapi vs Directus vs Payload CMS でContentful代替をセルフホスト

オープンソースラボ編集部2026年6月13日

Headless CMS OSS比較:Strapi vs Directus vs Payload CMS でContentful代替をセルフホスト

ContentfulはFreeプランでAPIリクエスト5万回/月・Proプランは月$300〜とエンタープライズに偏ったコスト構造です。Strapi(最も採用実績多・Node.js)・Directus(DB-First・SQLを直接管理)・Payload CMS(TypeScript-First・Next.jsと深く統合)はOSSのHeadless CMSで、Next.js/Nuxt.js/Astro等のフロントエンドにコンテンツAPIを提供できます。

Headless CMSの選定理由

  • コンテンツ管理のUI分離: エンジニアでないライターがコンテンツを管理できる管理画面を自前提供
  • REST/GraphQL API: フロントエンドフレームワークに依存しないAPIでコンテンツを配信
  • 型安全なコンテンツモデル: ブログ記事・商品・FAQ等のコンテンツ構造をスキーマで定義
  • メディア管理: 画像・動画をWebP変換・サイズ最適化付きで管理
  • 多言語対応: i18n対応でJA/EN等の複数言語コンテンツを管理

主要ツールの概要

Strapi

2015年からNode.js(TypeScript)で開発されているHeadless CMSです。GitHubスター63k+。プラグインエコシステム・カスタムAPI・ロールベースアクセス制御(RBAC)・GraphQL・i18nが標準搭載で、Headless CMSカテゴリで最も多くの商用採用実績を持ちます。

# Strapiプロジェクトを作成(SQLite・PostgreSQL対応)
npx create-strapi-app@latest my-cms   --dbclient=postgres   --dbhost=localhost   --dbport=5432   --dbname=strapi   --dbusername=strapi   --dbpassword=strapi_pass   --no-run

cd my-cms && npm run develop
# http://localhost:1337/admin でUI起動
// Strapi コンテンツタイプ定義(TypeScript)
// src/api/article/content-types/article/schema.json
{
  "kind": "collectionType",
  "collectionName": "articles",
  "info": {
    "singularName": "article",
    "pluralName": "articles",
    "displayName": "Article"
  },
  "options": {
    "draftAndPublish": true
  },
  "pluginOptions": {
    "i18n": { "localized": true }
  },
  "attributes": {
    "title": {
      "pluginOptions": { "i18n": { "localized": true } },
      "type": "string",
      "required": true
    },
    "content": {
      "pluginOptions": { "i18n": { "localized": true } },
      "type": "richtext"
    },
    "slug": {
      "type": "uid",
      "targetField": "title"
    },
    "cover": {
      "type": "media",
      "multiple": false,
      "required": false,
      "allowedTypes": ["images"]
    },
    "category": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "api::category.category"
    },
    "tags": {
      "type": "json"
    },
    "seo": {
      "type": "component",
      "repeatable": false,
      "component": "shared.seo"
    }
  }
}
// Next.js App Router + Strapi でコンテンツを取得
// lib/strapi.ts

const STRAPI_URL = process.env.STRAPI_URL!;
const STRAPI_TOKEN = process.env.STRAPI_API_TOKEN!;

async function fetchStrapi<T>(path: string, params?: Record<string, string>): Promise<T> {
  const qs = params ? '?' + new URLSearchParams(params).toString() : '';
  const res = await fetch(`${STRAPI_URL}/api${path}${qs}`, {
    headers: { Authorization: `Bearer ${STRAPI_TOKEN}` },
    next: { revalidate: 60 },  // ISR: 60秒ごとに再検証
  });
  const json = await res.json();
  return json.data;
}

// 記事一覧を取得
export async function getArticles(locale = 'ja') {
  return fetchStrapi<Article[]>('/articles', {
    'populate': 'cover,category,tags',
    'filters[publishedAt][$notNull]': 'true',
    'locale': locale,
    'sort': 'publishedAt:desc',
    'pagination[limit]': '20',
  });
}

// スラッグで記事を取得
export async function getArticleBySlug(slug: string, locale = 'ja') {
  const data = await fetchStrapi<Article[]>('/articles', {
    'filters[slug][$eq]': slug,
    'populate': 'cover,category,tags,seo',
    'locale': locale,
  });
  return data[0] || null;
}

// app/articles/[slug]/page.tsx
export default async function ArticlePage({ params }: { params: { slug: string } }) {
  const article = await getArticleBySlug(params.slug);
  if (!article) notFound();

  return (
    <article>
      <h1>{article.attributes.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.attributes.content }} />
    </article>
  );
}

Directus

2004年から開発されているDB-FirstのHeadless CMSです。GitHubスター27k+。既存のPostgreSQL/MySQL/SQLiteのテーブルをそのままCMS化できるのが特徴で、エンジニアが設計したデータベーススキーマをライターが使えるCMS管理画面に変換します。Strapi等とは逆のアプローチで「DBが主役・CMSが管理画面」という思想です。

# Directusをdocker-composeで起動
version: "3"

services:
  directus:
    image: directus/directus:10.11
    restart: unless-stopped
    ports:
      - "8055:8055"
    volumes:
      - directus_uploads:/directus/uploads
      - directus_extensions:/directus/extensions
    depends_on:
      database:
        condition: service_healthy
      cache:
        condition: service_started
    environment:
      SECRET: "your-directus-secret-key"
      DB_CLIENT: "pg"
      DB_HOST: "database"
      DB_PORT: "5432"
      DB_DATABASE: "directus"
      DB_USER: "directus"
      DB_PASSWORD: "directus_pass"
      CACHE_ENABLED: "true"
      CACHE_STORE: "redis"
      CACHE_REDIS: "redis://cache:6379"
      ADMIN_EMAIL: "admin@example.com"
      ADMIN_PASSWORD: "your-admin-password"
      PUBLIC_URL: "https://cms.yoursite.com"
      CORS_ENABLED: "true"
      CORS_ORIGIN: "https://yoursite.com"
      STORAGE_LOCATIONS: "local"
      STORAGE_LOCAL_DRIVER: "local"
      STORAGE_LOCAL_ROOT: "/directus/uploads"

  database:
    image: postgres:16-alpine
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'pg_isready', '-d', 'directus', '-U', 'directus']
      interval: 5s
      timeout: 5s
      retries: 5
    environment:
      POSTGRES_USER: directus
      POSTGRES_PASSWORD: directus_pass
      POSTGRES_DB: directus
    volumes:
      - directus_db:/var/lib/postgresql/data

  cache:
    image: redis:7-alpine
    restart: unless-stopped

volumes:
  directus_db:
  directus_uploads:
  directus_extensions:
// Next.js + Directus SDK でコンテンツを取得
// npm install @directus/sdk

import { createDirectus, rest, readItems, readItem, authentication } from '@directus/sdk';

const directus = createDirectus(process.env.DIRECTUS_URL!)
  .with(authentication('static', { token: process.env.DIRECTUS_TOKEN! }))
  .with(rest());

// 記事一覧取得
export async function getArticles() {
  return directus.request(readItems('articles', {
    fields: ['id', 'title', 'slug', 'summary', 'published_date', 'category.*', 'cover.*'],
    filter: { status: { _eq: 'published' } },
    sort: ['-published_date'],
    limit: 20,
  }));
}

// ウェブフックで変更時にNext.js ISRを再検証
// Directus Settings → Webhooks → POST https://yoursite.com/api/revalidate
// api/revalidate/route.ts
export async function POST(request: Request) {
  const secret = request.headers.get('x-webhook-secret');
  if (secret !== process.env.REVALIDATION_SECRET) {
    return new Response('Unauthorized', { status: 401 });
  }
  revalidatePath('/articles');
  revalidatePath('/');
  return Response.json({ revalidated: true });
}

Payload CMS

2021年からTypeScriptで開発されているコードファーストのHeadless CMSです。GitHubスター27k+。コレクション・フィールド・バリデーション・アクセス制御すべてをTypeScriptで定義し、型安全なSDKでNext.jsのサーバーコンポーネントから直接DBにアクセスできます。Next.js App Routerとの統合が最も深く、同じプロジェクト内にPayload CMSとNext.jsを共存させる「Payload + Next.js Monorepo」が特徴的です。

// payload.config.ts(Payload CMSの設定)
import { buildConfig } from 'payload/config';
import { postgresAdapter } from '@payloadcms/db-postgres';
import { lexicalEditor } from '@payloadcms/richtext-lexical';
import { s3Storage } from '@payloadcms/storage-s3';

export default buildConfig({
  serverURL: process.env.NEXT_PUBLIC_SERVER_URL!,
  admin: {
    user: 'users',
    bundler: viteBundler(),
  },
  db: postgresAdapter({
    pool: { connectionString: process.env.DATABASE_URL! },
  }),
  editor: lexicalEditor({}),
  plugins: [
    s3Storage({
      collections: { media: true },
      bucket: process.env.S3_BUCKET!,
      config: {
        credentials: { accessKeyId: process.env.S3_ACCESS_KEY!, secretAccessKey: process.env.S3_SECRET! },
        region: process.env.S3_REGION!,
      },
    }),
  ],
  collections: [
    {
      slug: 'articles',
      admin: {
        useAsTitle: 'title',
        defaultColumns: ['title', 'category', 'status', 'publishedAt'],
      },
      access: {
        read: () => true,         // 公開記事は誰でも読める
        create: isAdmin,
        update: isAdminOrAuthor,
        delete: isAdmin,
      },
      fields: [
        { name: 'title', type: 'text', required: true },
        { name: 'slug', type: 'text', unique: true, required: true, admin: { position: 'sidebar' } },
        { name: 'content', type: 'richText' },
        { name: 'category', type: 'relationship', relationTo: 'categories', required: true },
        { name: 'cover', type: 'upload', relationTo: 'media' },
        { name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft', admin: { position: 'sidebar' } },
        { name: 'publishedAt', type: 'date', admin: { position: 'sidebar' } },
      ],
      hooks: {
        afterChange: [revalidateArticle],  // 変更時にNext.js ISRを再検証
      },
    },
  ],
});

機能比較表

比較項目StrapiDirectusPayload CMS
ライセンスSSPL(v5)BUSLMIT
言語JavaScript/TSJavaScript/TSTypeScript
DB-First
型安全SDK部分的✅ 完全
Next.js統合✅ 最深
GraphQL
i18n✅ 標準プラグイン
GitHub Stars63k+27k+27k+

Headless CMSとCDNの組み合わせはDevOpsカテゴリ/categories/devopsを参照。コンテンツ管理と組み合わせるナレッジベースツールは/categories/knowledgeにまとめています。

FAQ

Q. StrapiとPayload CMSの最大の違いは何ですか?

A. 設計思想が根本的に異なります。Strapi: 「ノーコードでコンテンツモデルを定義」 - 管理UIからコレクションを作成し、生成されたAPIを使う。エンジニアでなくても管理できる。プロジェクトが成長するにつれてカスタマイズが難しくなることがある。Payload CMS: 「コードでコンテンツモデルを定義(Configuration as Code)」 - TypeScriptで型安全にコレクション・フィールド・バリデーション・アクセス制御を定義する。DBスキーマはPayloadが自動生成。Gitで管理でき、チームで変更をレビューできる。選択基準: CMSの設定をコードで管理したい・Next.js App Routerと最深レベルで統合したい→Payload。ライター/非エンジニアが管理UI上でコンテンツモデルを変更できるようにしたい・プラグインエコシステムが重要→Strapi。

Q. DirectusでSupabase(PostgreSQL)のテーブルを直接Headless CMS化できますか?

A. Directusは既存のPostgreSQLデータベースに接続してそのテーブルを管理UIで操作できます。Supabaseとの接続方法: SupabaseのSettings → Database → Connection InfoでPostgres接続文字列を取得し、DirectusのDB設定に使います。注意点: ①Directusはインストール時にシステムテーブル(directus_users・directus_collections等)をDBに追加します②SupabaseのRowLevelSecurity(RLS)はDirectusのサーバー側接続(supabaseサービスロール)では無効化されます③Supabaseのネイティブ認証(Auth)とDirectusのAuth(独自)は独立しているためユーザー管理が二重になります。推奨構成: SupabaseをDBとしてのみ使い、認証・管理UIはDirectusに統一するか、もしくはSupabaseのAuth+Directusを別DBで動かすハイブリッドです。

Q. Payload CMSをNext.js App Routerと同じプロジェクト内に共存させる方法は?

A. Payload CMS v3はNext.js App RouterとのMonorepo共存をサポートしています。セットアップ:

# Payload Next.jsテンプレートで新規プロジェクト作成
npx create-payload-app@latest my-site --template website
cd my-site && pnpm install && pnpm run dev
# CMSと Next.js が http://localhost:3000 で同時起動
# /admin でPayload CMS管理画面
# / でNext.jsフロントエンド

この構成の利点: ①DBアクセスが直接(APIリクエスト不要)でサーバーコンポーネントからpayload.find({collection: 'articles'})を直接呼び出せる②Next.jsとPayloadが同じTypeScript型を共有③デプロイがシングルVercel/Railway デプロイで完結。制限: VercelのEdge Runtimeでは動作しないためruntime = 'nodejs'が必要です。

Q. ContentfulからStrapiへの移行手順を教えてください。

A. Contentfulからのコンテンツ移行は「スキーマ移行」と「コンテンツデータ移行」の2ステップです。①スキーマ移行: ContentfulのContent Model(JSON)をエクスポート→Strapi管理UIでコレクションとして手動再作成(Content TypeのフィールドはほぼStrapiで対応するフィールドタイプが存在)②コンテンツ移行: Contentful Management APIで全エントリをJSONエクスポート→変換スクリプト(contentful-field名→strapi-field名マッピング)→Strapi REST APIでバルクインポート。メディアファイル: ContentfulのMediaをダウンロードしてStrapiのMedia Libraryにアップロード(スクリプトで自動化)。フロントエンドの変更: contentful SDKを @strapi/sdk に差し替えてAPIエンドポイントを変更。Contentfulのrich textはStrapiのMarkdownまたはRichtextに変換。

まとめ

ユースケース推奨ツール
最多採用実績・プラグイン豊富Strapi
既存DBをCMS化・DB-FirstDirectus
Next.js深統合・TypeScript型安全Payload CMS
Contentful代替を今すぐ始めたいStrapi

関連外部リソース

他の記事も読む

Let's Build Together

OSS導入、自社だけで悩まない。

ツール選定から構築・運用・AI活用まで、オープンソースラボ運営元のClasslessが伴走します。初回のご相談は無料です。