payload-cms について
payload-cms は、Payload CMS の開発を支援するスキルです。Next.js と TypeScript で CMS を構築する開発者向けに、コレクション設定、フック、アクセス制御、クエリ、セキュリティのパターンを扱います。
主な機能
- 01コレクション、フィールド、グローバルの設定パターン
- 02ライフサイクルフックとデータ処理
- 03アクセス制御とユーザー権限
- 04Local API クエリとリレーションフィールド
- 05トランザクション整合性とフックループの防止
活用シーン
- 01コンテンツコレクションのフィールドと下書き公開の設定
- 02ユーザー権限に基づくデータアクセス制限
- 03トランザクションを保ちながらフックでデータ処理
- 04Local API のアクセス制御問題の診断
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ connorads/ dotfiles --skill payload-cmsインストール後、Claude Code で/payload-cmsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
AGENTS.mdREADME.mdreferences/access-control.mdadvanced.mdcollections.mdfields.mdhooks.mdqueries.md詳細
Payload CMS の開発
Payload は、TypeScript を第一に考えたアーキテクチャを備える Next.js ネイティブの CMS です。このスキルでは、コレクション、フック、アクセス制御、クエリを適切に構築するための専門知識を身につけられます。
メンタルモデル
Payload は、相互に連携する3つのレイヤーとして考えてください。
- 設定レイヤー → コレクション、グローバル、フィールドでスキーマを定義します
- フックレイヤー → ライフサイクルイベントでデータを変換、検証します
- アクセスレイヤー → 関数で操作できるユーザーと内容を制御します
すべての操作は、次の流れで実行されます:Config → Access Check → Hook Chain → Database → Response Hooks
クイックリファレンス
| タスク | 解決策 | 詳細 |
|---|---|---|
| スラッグを自動生成する | slugField() または beforeChange フック | [references/fields.md#slug-field] |
| ユーザーごとに制限する | クエリ制約を使ったアクセス制御 | [references/access-control.md] |
| 認証付きの Local API | user + 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を設定している
リファレンスドキュメント
詳しいパターンについては、以下を参照してください。
- references/fields.md - すべてのフィールド型、検証、条件付きロジック
- references/collections.md - 認証、アップロード、下書き、ライブプレビュー
- references/hooks.md - フックのライフサイクル、コンテキスト、パターン
- references/access-control.md - RBAC、行レベル、フィールドレベル
- references/queries.md - 演算子、Local/REST/GraphQL API
- references/advanced.md - ジョブ、プラグイン、ローカライズ
Resources
- Docs: https://payloadcms.com/docs
- LLM Context: https://payloadcms.com/llms-full.txt
- GitHub: https://github.com/payloadcms/payload
- Templates: https://github.com/payloadcms/payload/tree/main/templates
コメント
Markdown 対応