payload について
payloadは、Next.jsネイティブのCMSであるPayloadの開発を支援する、TypeScript優先のスキルです。コンテンツ設計、アクセス制御、クエリ、フック、プラグインの実装パターンを扱います。
主な機能
- 01コレクションとフィールドの設定
- 02ドラフト、バージョン、ローカライズの管理
- 03アクセス制御とクエリの設定
- 04フックとカスタムAPIルートの作成
- 05プラグイン開発と型生成
活用シーン
- 01ドラフト・公開フローを備えたCMSの構築
- 02ユーザー権限に応じたコンテンツ制限
- 03計算フィールドとフックの実装
- 04プラグインによるPayloadの拡張
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ payloadcms/ skills --skill payloadインストール後、Claude Code で/payloadを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.mdreference/ACCESS-CONTROL-ADVANCED.mdACCESS-CONTROL.mdADAPTERS.mdADVANCED.mdCOLLECTIONS.mdENDPOINTS.mdFIELD-TYPE-GUARDS.mdFIELDS.mdHOOKS.mdPLUGIN-DEVELOPMENT.mdQUERIES.md詳細
Payload アプリケーション開発
Payload は Next.js ネイティブの CMS で、TypeScript ファーストのアーキテクチャを採用しており、管理パネル、データベース管理、REST/GraphQL API、認証、ファイルストレージを提供します。
クイックリファレンス
クイックスタート
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'
import path from 'path'
import { fileURLToPath } from 'url'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
admin: {
user: 'users',
importMap: {
baseDir: path.resolve(dirname),
},
},
collections: [Users, Media],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET,
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
db: mongooseAdapter({
url: process.env.DATABASE_URL,
}),
})
重要なパターン
デフォルトと慣例
明確な理由がない限り、コンテンツをモデル化する際は次のデフォルトを適用してください。
- デフォルトで下書き/バージョンを有効にする:
versions: { drafts: true }。これは、あらゆるコンテンツコレクションで推奨される開始点です。_statusフィールド(draft/published/changed)が自動的に挿入されます。**独自のstatusフィールドは追加しないでください。**重複するためです。公開/下書きのライフサイクルがないコレクション(内部の結合テーブル、設定など)に限り、バージョンを省略してください。 - すべてのスラッグには、独自に
{ name: 'slug', type: 'text', unique: true }を作成するのではなく、slugField()を使用してください。タイトルからスラッグを自動生成し、再生成の切り替えを追加して、一意性とインデックス作成を処理します。デフォルトではtitleフィールドから生成します。コレクションにtitleがない場合は、ソースフィールドを渡してください:slugField({ useAsSlug: 'name' })。 position: 'sidebar'は、ひと目で確認できる短いフィールド(ステータス、カテゴリー、作成者、公開日)に使用します。使いやすさに横幅が必要な長いフィールド(説明、リッチテキストコンテンツ、長文テキスト)には使用しないでください。これらはメインのドキュメント領域に配置します。
基本的なコレクション
import type { CollectionConfig } from 'payload'
import { slugField } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
// _status (from versions.drafts) shows the draft/published state — no custom status field needed
defaultColumns: ['title', 'author', '_status', 'createdAt'],
},
versions: {
drafts: true,
},
fields: [
{ name: 'title', type: 'text', required: true },
slugField(), // auto-generates from `title`, unique + indexed, sidebar position
{ name: 'content', type: 'richText' }, // long field — stays in the main area, not the sidebar
// short, at-a-glance field — good sidebar candidate
{ name: 'author', type: 'relationship', relationTo: 'users', admin: { position: 'sidebar' } },
],
timestamps: true,
}
その他のコレクションパターン(認証、アップロード、下書き、ライブプレビュー)については、COLLECTIONS.md を参照してください。
よく使うフィールド
// Text field
{ name: 'title', type: 'text', required: true }
// Relationship
{ name: 'author', type: 'relationship', relationTo: 'users', required: true }
// Rich text
{ name: 'content', type: 'richText', required: true }
// Slug — use the helper instead of a hand-rolled text field
slugField()
// Select (for genuine taxonomy — NOT publish state; use versions.drafts + _status for that)
{ name: 'category', type: 'select', options: ['news', 'tutorial', 'opinion'] }
// Upload
{ name: 'image', type: 'upload', relationTo: 'media' }
すべてのフィールドタイプ(array、blocks、point、join、virtual、conditional など)については、FIELDS.md を参照してください。
フックの例
フックには 2 つのレベルがあり、相互に置き換えることはできません。コレクションフックは { doc, data, req, operation, ... } を受け取り、ドキュメント全体に作用します。フィールドフックは個々のフィールドの hooks オブジェクト内にあり、{ value, siblingData, ... } を受け取り、そのフィールドの新しい値を返します。計算フィールド/仮想フィールド、フィールドごとのフォーマッター、フィールドごとのアクセスマスキングにはフィールドフックを使用します。フィールドをまたぐビジネスロジックにはコレクションフックを使用します。
// Collection-level: business logic across the document
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
beforeChange: [
async ({ data, operation }) => {
if (operation === 'create') {
data.slug = slugify(data.title)
}
return data
},
],
},
fields: [{ name: 'title', type: 'text' }],
}
// Field-level: compute / format a single field's value (virtual fields use this)
export const Users: CollectionConfig = {
slug: 'users',
fields: [
{ name: 'firstName', type: 'text' },
{ name: 'lastName', type: 'text' },
{
name: 'fullName',
type: 'text',
virtual: true,
hooks: {
afterRead: [({ siblingData }) => `${siblingData.firstName} ${siblingData.lastName}`],
},
},
],
}
「フィールドを計算する」または「フックでフィールドの値を設定する」よう求められた場合は、そのフィールドにフィールドレベルのフックを使用してください。doc を変更するコレクションレベルの afterRead は使用しないでください。
すべてのフックパターンについては、HOOKS.md を参照してください。アクセス制御については、ACCESS-CONTROL.md を参照してください。
型安全なアクセス制御
import type { Access } from 'payload'
import type { User } from '@/payload-types'
// Type-safe access control
export const adminOnly: Access = ({ req }) => {
const user = req.user as User
return user?.roles?.includes('admin') || false
}
// Row-level access control
export const ownPostsOnly: Access = ({ req }) => {
const user = req.user as User
if (!user) return false
if (user.roles?.includes('admin')) return true
return {
author: { equals: user.id },
}
}
クエリの例
// Local API
const posts = await payload.find({
collection: 'posts',
where: {
status: { equals: 'published' },
'author.name': { contains: 'john' },
},
depth: 2,
limit: 10,
sort: '-createdAt',
})
// Query with populated relationships
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 2, // Populates relationships (default is 2)
})
// Returns: { author: { id: "user123", name: "John" } }
// Without depth, relationships return IDs only
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 0,
})
// Returns: { author: "user123" }
すべてのクエリ演算子と REST/GraphQL の例については、QUERIES.md を参照してください。
Payload インスタンスの取得
// In API routes (Next.js)
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
import { getPayload } from 'payload'
import config from '@payload-config'
export default async function Page() {
const payload = await getPayload({ config })
const { docs } = await payload.find({ collection: 'posts' })
return <div>{docs.map(post => <h1 key={post.id}>{post.title}</h1>)}</div>
}
セキュリティ上の注意点
1. Local API のアクセス制御(重大)
デフォルトでは、ユーザーを渡した場合でも、Local API の操作はすべてのアクセス制御をバイパスします。
// ❌ SECURITY BUG: Passes user but ignores their permissions
await payload.find({
collection: 'posts',
user: someUser, // Access control is BYPASSED!
})
// ✅ SECURE: Actually enforces the user's permissions
await payload.find({
collection: 'posts',
user: someUser,
overrideAccess: false, // REQUIRED for access control
})
使い分け:
overrideAccess: true(デフォルト):信頼できるサーバー側の操作(cron ジョブ、システムタスク)overrideAccess: false:ユーザーに代わって操作する場合(API ルート、Webhook)
QUERIES.md#access-control-in-local-api を参照してください。
2. フック内のトランザクション失敗
フック内のネストされた操作で req を渡さないと、トランザクションの原子性が損なわれます。
// ❌ DATA CORRUPTION RISK: Separate transaction
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { docId: doc.id },
// Missing req - runs in separate transaction!
})
},
]
}
// ✅ ATOMIC: Same transaction
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { docId: doc.id },
req, // Maintains atomicity
})
},
]
}
ADAPTERS.md#threading-req-through-operations を参照してください。
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: Use context flag
hooks: {
afterChange: [
async ({ doc, req, context }) => {
if (context.skipHooks) return
await req.payload.update({
collection: 'posts',
id: doc.id,
data: { views: doc.views + 1 },
context: { skipHooks: true },
req,
})
},
]
}
HOOKS.md#context を参照してください。
プロジェクト構成
src/
├── app/
│ ├── (frontend)/
│ │ └── page.tsx
│ └── (payload)/
│ └── admin/[[...segments]]/page.tsx
├── collections/
│ ├── Posts.ts
│ ├── Media.ts
│ └── Users.ts
├── globals/
│ └── Header.ts
├── components/
│ └── CustomField.tsx
├── hooks/
│ └── slugify.ts
└── payload.config.ts
ビルドと型生成
Payload は payload-types.ts を生成するため、手動で generate:types を実行する必要はほとんどありません。
- 開発中:
typescript.autoGenerateのデフォルト値はtrueなので、設定を変更するたびに開発サーバーが型を自動的に再生成します。開発サーバーの実行中にgenerate:typesを手動で実行しないでください。重複した処理になります。 - ビルド中:
payload buildはnext buildの実行前にインポートマップと型を生成します。どちらも古い状態にならないよう、next buildを直接呼び出すよりこちらを優先してください。型生成をスキップするには--no-typesを渡します。 - 手動生成(
payload generate:types)は、開発サーバーもビルドも実行されていない場合に限る最終手段です(例:単発のスクリプト、またはpayload buildを実行しないステップの前に CI で実行する場合)。
// payload.config.ts
export default buildConfig({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
// autoGenerate defaults to true — types regenerate in dev automatically
},
})
// Usage
import type { Post, User } from '@/payload-types'
よくある注意点
- Local API はアクセス制御をバイパスします。
overrideAccess: falseを渡した場合を除きます - ネストされた操作で
reqが不足すると、トランザクションのアトミック性が損なわれます - フックのループ — フック内の操作によって同じフックが再実行されることがあります。
req.contextフラグを使用してください - フィールドレベルのアクセス制御が返すのはブール値のみで、クエリ制約は指定できません
- リレーションシップの深さのデフォルトは 2 です。ID のみを取得する場合は
depth: 0を設定してください - 下書きステータス — 下書きを有効にすると、
_statusフィールドが自動的に挿入されます - 型は開発中(
autoGenerate)およびpayload buildの実行中に自動再生成されます。generate:typesの手動実行は避けてください - MongoDB のトランザクションにはレプリカセットの設定が必要です
- SQLite のトランザクションはデフォルトで無効です。
transactionOptions: {}で有効にしてください - Point フィールドは SQLite ではサポートされていません
ベストプラクティス
コンテンツモデリング
- コンテンツコレクションでは、デフォルトで
versions: { drafts: true }を有効にしてください。カスタムのstatusフィールドを追加するのではなく、自動挿入される_statusフィールドを使用します - 独自に一意のテキストフィールドを作成するのではなく、スラッグには
slugField()を使用してください position: 'sidebar'は、ひと目で確認できる短いフィールド(ステータス、カテゴリー、作成者、日付)に使用し、長いフィールド(説明、リッチテキスト)はメイン領域に配置してください
セキュリティ
- まず制限の厳しいアクセス制御を設定し、権限を段階的に追加してください
- Local API に
userを渡す場合は、overrideAccess: falseを使用してください - フィールドレベルのアクセス制御が返すのはブール値のみで、クエリ制約は指定できません
- クライアントから渡されたデータを決して信用しないでください
- データベースへの参照を避けるため、ロールには
saveToJWT: trueを使用してください
パフォーマンス
- 頻繁にクエリされるフィールドにインデックスを設定してください
- 返されるフィールドを制限するには
selectを使用してください - 過剰なデータ取得を防ぐため、リレーションシップに
maxDepthを設定してください - アクセス制御では、非同期処理よりクエリ制約を優先してください
- コストの高い処理は
req.contextにキャッシュしてください
データ整合性
- フック内のネストされた操作には必ず
reqを渡してください - 無限のフックループを防ぐため、コンテキストフラグを使用してください
- MongoDB(レプリカセットが必要)と Postgres ではトランザクションを有効にしてください
- データの形式を整えるには
beforeValidateを使用してください - ビジネスロジックには
beforeChangeを使用してください
型安全性
- 開発中(
autoGenerate)およびpayload buildに型を生成させてください。generate:typesを手動で実行するのは、どちらも実行されていない場合に限ります - 生成された
payload-types.tsから型をインポートしてください - ユーザーオブジェクトに型を指定してください:
import type { User } from '@/payload-types' - 実行時の型チェックにはフィールド型ガードを使用してください
- Payload の値(コレクション、フィールド、フック、アクセス関数、プラグインなど)を名前付き定数に抽出する場合は、対応する Payload の型(
CollectionConfig、Field、CollectionBeforeChangeHook、Access、Pluginなど)で注釈を付けるか、satisfies <Type>を使用してください。注釈がないと、type: 'text'のような文字列プロパティがstringに拡大され、判別共用体(Field、CollectionConfig)を解決できなくなります。インラインリテラルではコンテキスト型付けによって自動的に型が適用されますが、抽出した定数には適用されません。
構成
- コレクションは別々のファイルに分けてください
- アクセス制御は
access/ディレクトリに分離してください - フックは
hooks/ディレクトリに分離してください - よく使うパターンには再利用可能なフィールドファクトリーを使用してください
- 複雑なアクセス制御にはコメントで説明を加えてください
リファレンスドキュメント
- FIELDS.md - すべてのフィールドタイプ、バリデーション、管理オプション
- FIELD-TYPE-GUARDS.md - 実行時のフィールドタイプのチェックと絞り込みに使用する型ガード
- COLLECTIONS.md - コレクション設定、認証、アップロード、下書き、ライブプレビュー
- HOOKS.md - コレクションフック、フィールドフック、コンテキストパターン
- ACCESS-CONTROL.md - コレクション、フィールド、グローバルのアクセス制御、RBAC、マルチテナント
- ACCESS-CONTROL-ADVANCED.md - コンテキスト対応、時間ベース、サブスクリプションベースのアクセス、ファクトリー関数、テンプレート
- QUERIES.md - クエリー演算子、Local/REST/GraphQL API
- ENDPOINTS.md - カスタム API エンドポイント:認証、ヘルパー、リクエスト/レスポンスのパターン
- ADAPTERS.md - データベース、ストレージ、メールのアダプター、トランザクション
- ADVANCED.md - 認証、ジョブ、エンドポイント、コンポーネント、プラグイン、ローカライズ
- PLUGIN-DEVELOPMENT.md - プラグインアーキテクチャ、モノレポ構造、パターン、ベストプラクティス