mcpField
P
payload-cms

payload-cms

github.com/connorads開発18240 回インストール今日更新
リポジトリ

payload-cms について

payload-cms は、Payload CMS の開発を支援するスキルです。Next.js と TypeScript で CMS を構築する開発者向けに、コレクション設定、フック、アクセス制御、クエリ、セキュリティのパターンを扱います。

主な機能

  1. 01コレクション、フィールド、グローバルの設定パターン
  2. 02ライフサイクルフックとデータ処理
  3. 03アクセス制御とユーザー権限
  4. 04Local API クエリとリレーションフィールド
  5. 05トランザクション整合性とフックループの防止

活用シーン

  1. 01コンテンツコレクションのフィールドと下書き公開の設定
  2. 02ユーザー権限に基づくデータアクセス制限
  3. 03トランザクションを保ちながらフックでデータ処理
  4. 04Local API のアクセス制御問題の診断

インストール

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

$npx skills add https://github.com/connorads/dotfiles --skill payload-cms

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

同梱ファイル

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

AGENTS.md
README.md
references/
access-control.md
advanced.md
collections.md
fields.md
hooks.md
queries.md

詳細

Payload CMS の開発

Payload は、TypeScript を第一に考えたアーキテクチャを備える Next.js ネイティブの CMS です。このスキルでは、コレクション、フック、アクセス制御、クエリを適切に構築するための専門知識を身につけられます。

メンタルモデル

Payload は、相互に連携する3つのレイヤーとして考えてください。

  1. 設定レイヤー → コレクション、グローバル、フィールドでスキーマを定義します
  2. フックレイヤー → ライフサイクルイベントでデータを変換、検証します
  3. アクセスレイヤー → 関数で操作できるユーザーと内容を制御します

すべての操作は、次の流れで実行されます:Config → Access Check → Hook Chain → Database → Response Hooks

クイックリファレンス

タスク解決策詳細
スラッグを自動生成するslugField() または beforeChange フック[references/fields.md#slug-field]
ユーザーごとに制限するクエリ制約を使ったアクセス制御[references/access-control.md]
認証付きの Local APIuser + overrideAccess: false[references/queries.md#local-api]
下書き/公開versions: { drafts: true }[references/collections.md#drafts]
計算フィールドafterRead フックを使う virtual: true[references/fields.md#virtual]
条件付きフィールドadmin.condition[references/fields.md#conditional]
リレーションシップをフィルタリングするフィールドの filterOptions[references/fields.md#relationship]
フックのループを防ぐreq.context フラグ[references/hooks.md#context]
トランザクションすべての操作に req を渡す[references/hooks.md#transactions]
バックグラウンドジョブタスクを使ったジョブキュー[references/advanced.md#jobs]

クイックスタート

npx create-payload-app@latest my-app
cd my-app
pnpm dev
最小限の設定
import { buildConfig } from 'payload'
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { lexicalEditor } from '@payloadcms/richtext-lexical'

export default buildConfig({
  admin: { user: 'users' },
  collections: [Users, Media, Posts],
  editor: lexicalEditor(),
  secret: process.env.PAYLOAD_SECRET,
  typescript: { outputFile: 'payload-types.ts' },
  db: mongooseAdapter({ url: process.env.DATABASE_URL }),
})

基本パターン

コレクションの定義
import type { CollectionConfig } from 'payload'

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    useAsTitle: 'title',
    defaultColumns: ['title', 'author', 'status', 'createdAt'],
  },
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'slug', type: 'text', unique: true, index: true },
    { name: 'content', type: 'richText' },
    { name: 'author', type: 'relationship', relationTo: 'users' },
    { name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' },
  ],
  timestamps: true,
}
フックのパターン(スラッグの自動生成)
export const Posts: CollectionConfig = {
  slug: 'posts',
  hooks: {
    beforeChange: [
      async ({ data, operation }) => {
        if (operation === 'create' && data.title) {
          data.slug = data.title.toLowerCase().replace(/\s+/g, '-')
        }
        return data
      },
    ],
  },
  fields: [{ name: 'title', type: 'text', required: true }],
}
アクセス制御のパターン
import type { Access } from 'payload'

// Type-safe: admin-only access
export const adminOnly: Access = ({ req }) => {
  return req.user?.roles?.includes('admin') ?? false
}

// Row-level: users see only their own posts
export const ownPostsOnly: Access = ({ req }) => {
  if (!req.user) return false
  if (req.user.roles?.includes('admin')) return true
  return { author: { equals: req.user.id } }
}
クエリのパターン
// Local API with access control
const posts = await payload.find({
  collection: 'posts',
  where: {
    status: { equals: 'published' },
    'author.name': { contains: 'john' },
  },
  depth: 2,
  limit: 10,
  sort: '-createdAt',
  user: req.user,
  overrideAccess: false, // CRITICAL: enforce permissions
})

重要なセキュリティルール

1. Local API のアクセス制御

デフォルトでは、すべてのアクセス制御が回避されます。 これは最もよくあるセキュリティ上のミスです。

// ❌ SECURITY BUG: Access control bypassed even with user
await payload.find({ collection: 'posts', user: someUser })

// ✅ SECURE: Explicitly enforce permissions
await payload.find({
  collection: 'posts',
  user: someUser,
  overrideAccess: false, // REQUIRED
})

ルール: ユーザーに代わって実行する操作には、必ず overrideAccess: false を使用してください。

2. トランザクションの整合性

req を指定しない操作は、別々のトランザクションで実行されます。

// ❌ DATA CORRUPTION: Separate transaction
hooks: {
  afterChange: [async ({ doc, req }) => {
    await req.payload.create({
      collection: 'audit-log',
      data: { docId: doc.id },
      // Missing req - breaks atomicity!
    })
  }]
}

// ✅ ATOMIC: Same transaction
hooks: {
  afterChange: [async ({ doc, req }) => {
    await req.payload.create({
      collection: 'audit-log',
      data: { docId: doc.id },
      req, // Maintains transaction
    })
  }]
}

ルール: フック内のネストされた操作には、必ず req を渡してください。

3. 無限フックループ

フックが自分自身を呼び出すと、無限ループが発生します。

// ❌ INFINITE LOOP
hooks: {
  afterChange: [async ({ doc, req }) => {
    await req.payload.update({
      collection: 'posts',
      id: doc.id,
      data: { views: doc.views + 1 },
      req,
    }) // Triggers afterChange again!
  }]
}

// ✅ SAFE: Context flag breaks the loop
hooks: {
  afterChange: [async ({ doc, req, context }) => {
    if (context.skipViewUpdate) return
    await req.payload.update({
      collection: 'posts',
      id: doc.id,
      data: { views: doc.views + 1 },
      req,
      context: { skipViewUpdate: true },
    })
  }]
}

プロジェクト構成

src/
├── app/
│   ├── (frontend)/page.tsx
│   └── (payload)/admin/[[...segments]]/page.tsx
├── collections/
│   ├── Posts.ts
│   ├── Media.ts
│   └── Users.ts
├── globals/Header.ts
├── hooks/slugify.ts
└── payload.config.ts

型の生成

スキーマを変更したら、型を生成します。

// payload.config.ts
export default buildConfig({
  typescript: { outputFile: 'payload-types.ts' },
})

// Usage
import type { Post, User } from '@/payload-types'

Payload インスタンスの取得

// In API routes
import { getPayload } from 'payload'
import config from '@payload-config'

export async function GET() {
  const payload = await getPayload({ config })
  const posts = await payload.find({ collection: 'posts' })
  return Response.json(posts)
}

// In Server Components
export default async function Page() {
  const payload = await getPayload({ config })
  const { docs } = await payload.find({ collection: 'posts' })
  return <div>{docs.map(p => <h1 key={p.id}>{p.title}</h1>)}</div>
}

一般的なフィールドの型

// Text
{ name: 'title', type: 'text', required: true }

// Relationship
{ name: 'author', type: 'relationship', relationTo: 'users' }

// Rich text
{ name: 'content', type: 'richText' }

// Select
{ name: 'status', type: 'select', options: ['draft', 'published'] }

// Upload
{ name: 'image', type: 'upload', relationTo: 'media' }

// Array
{
  name: 'tags',
  type: 'array',
  fields: [{ name: 'tag', type: 'text' }],
}

// Blocks (polymorphic content)
{
  name: 'layout',
  type: 'blocks',
  blocks: [HeroBlock, ContentBlock, CTABlock],
}

判断の指針

アプローチを選ぶ際の目安:

シナリオアプローチ
保存前のデータ変換beforeChange フック
読み取り後のデータ変換afterRead フック
ビジネスルールの適用アクセス制御関数
複雑な検証フィールドの validate 関数
計算された表示値afterRead を使う仮想フィールド
関連ドキュメントの一覧join フィールド型
副作用(メール、Webhook)コンテキストガード付きの afterChange フック
データベースレベルの制約unique: true または index: true を設定したフィールド

品質チェック

良質な Payload コード:

  • ユーザーコンテキストを使うすべての Local API 呼び出しで overrideAccess: false を指定している
  • トランザクションの整合性を保つため、すべてのフック操作に req を渡している
  • 再帰的なフックで context フラグを使用している
  • 型を生成し、payload-types.ts からインポートしている
  • アクセス制御関数に Access 型を指定している
  • コレクションに意味のある admin.useAsTitle を設定している

リファレンスドキュメント

詳しいパターンについては、以下を参照してください。

Resources

コメント