payload について
Payload アプリケーション開発向けのスキルです。Next.js と TypeScript のプロジェクトで使う CMS 設定、クエリ、フック、アクセス制御、プラグインのパターンを扱います。
主な機能
- 01コレクションとフィールドの設計
- 02下書き、バージョン、スラッグ
- 03クエリとアクセス制御
- 04フックとトランザクションの指針
- 05プラグインと API のパターン
活用シーン
- 01コンテンツコレクションの設定
- 02ロール別コンテンツアクセスの実装
- 03フックとバックグラウンドジョブの追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ payloadcms/ payload --skill payloadインストール後、Claude Code で/payloadを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
reference/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、認証、ファイルストレージを提供します。
クイックリファレンス
| タスク | 解決策 | 詳細 |
|---|---|---|
| スラッグの自動生成 | { type: 'slug', useAsSlug: 'title' } | FIELDS.md#slug-field |
| ユーザーによるコンテンツの制限 | クエリによるアクセス制御 | ACCESS-CONTROL.md#row-level-security-with-complex-queries |
| ローカル API のユーザー操作 | user + overrideAccess: false | QUERIES.md#access-control-in-local-api |
| 下書き/公開ワークフロー | versions: { drafts: true } | COLLECTIONS.md#versioning--drafts |
| 計算フィールド | 値を返すフィールドレベルの hooks.afterRead を伴う virtual: true | FIELDS.md#virtual-fields |
| ドキュメントのタイトル | admin.useAsTitle に保存されたトップレベルのフィールド | COLLECTIONS.md#useastitle |
| 条件付きフィールド | admin.condition | FIELDS.md#conditional-fields |
| カスタムフィールドのバリデーション | validate 関数 | FIELDS.md#validation |
| リレーションシップリストのフィルタリング | フィールドの filterOptions | FIELDS.md#relationship |
| 特定のフィールドの選択 | select パラメータ | QUERIES.md#field-selection |
| 作成者/日付の自動設定 | beforeChange フック | HOOKS.md#collection-hooks |
| フックのループの防止 | req.context チェック | HOOKS.md#context |
| カスケード削除 | beforeDelete フック | HOOKS.md#collection-hooks |
| 地理空間クエリ | near/within を伴う point フィールド | FIELDS.md#point-geolocation |
| 逆リレーションシップ | join フィールドタイプ | FIELDS.md#join-fields |
| Next.js の再検証 | afterChange でのコンテキスト制御 | HOOKS.md#nextjs-revalidation-with-context-control |
| リレーションシップによるクエリ | ネストされたプロパティ構文 | QUERIES.md#nested-properties |
| 複雑なクエリ | AND/OR ロジック | QUERIES.md#andor-logic |
| トランザクション | 操作に req を渡す | ADAPTERS.md#threading-req-through-operations |
| バックグラウンドジョブ | タスクを伴うジョブキュー | ADVANCED.md#jobs-queue |
| カスタム API ルート | コレクションのカスタムエンドポイント | ADVANCED.md#custom-endpoints |
| クラウドストレージ | ストレージアダプタプラグイン | ADAPTERS.md#storage-adapters |
| 多言語対応 | localization 設定 + localized: true | ADVANCED.md#localization |
| プラグインの作成 | (options) => (config) => Config | PLUGIN-DEVELOPMENT.md#plugin-architecture |
| プラグインパッケージのセットアップ | SWC を使用したパッケージ構造 | PLUGIN-DEVELOPMENT.md#plugin-package-structure |
| コレクションへのフィールドの追加 | コレクションのマッピング、フィールドの展開 | PLUGIN-DEVELOPMENT.md#adding-fields-to-collections |
| プラグインフック | 配列内の既存のフックの保持 | PLUGIN-DEVELOPMENT.md#adding-hooks |
| フィールドタイプの確認 | 型ガード関数 | FIELD-TYPE-GUARDS.md |
クイックスタート
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フィールドを追加しないでください、冗長です。バージョンをスキップするのは、 公開/下書きのライフサイクルを持たないコレクション(例:内部の結合テーブル、設定)だけにしてください。 - すべてのスラッグにはネイティブの
slugフィールド型を使用する。手書きの{ name: 'slug', type: 'text', unique: true }は使わないでください。これはソースフィールドから スラッグを自動生成し、再生成トグルを追加し、デフォルトでrequired、unique、index、position: 'sidebar'になります。useAsSlugは必須です — 生成元の ソースフィールドを指定してください:{ name: 'slug', type: 'slug', useAsSlug: 'title' }。 position: 'sidebar'は短く一目でわかるフィールド向け — ステータス、カテゴリ、 著者、公開日など。使うのに横幅が必要な長いフィールド(説明、リッチテキスト、 長文テキスト)には避けてください。それらはメインドキュメント領域に属します。admin.useAsTitleには保存されるトップレベルフィールドを使用する。virtual: trueで 設定された計算フィールドをadmin.useAsTitleに決して設定しないでください。これらの フィールドはクエリ不可であり、Payload はその設定を拒否します。virtual: 'author.name'のような リレーションシップパスの仮想フィールドはサポートされていますが、タイトルが関連ドキュメントから 取得される必要がある場合にのみ使用してください。
基本的なコレクション
import type { CollectionConfig } 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 },
{ name: 'slug', type: 'slug', useAsSlug: 'title' }, // 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 native field type instead of a hand-rolled text field
{ name: 'slug', type: 'slug', useAsSlug: 'title' }
// 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',
overrideAccess: true,
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',
overrideAccess: true,
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',
overrideAccess: true,
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(request: Request) {
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers: request.headers })
const posts = await payload.find({
collection: 'posts',
overrideAccess: false, // this route answers for whoever called it
user,
})
return Response.json(posts)
}
// In Server Components
import { headers } from 'next/headers'
import { getPayload } from 'payload'
import config from '@payload-config'
export default async function Page() {
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers: await headers() })
const { docs } = await payload.find({
collection: 'posts',
overrideAccess: false,
user,
})
return <div>{docs.map(post => <h1 key={post.id}>{post.title}</h1>)}</div>
}
セキュリティ上の落とし穴
1. Local API のアクセス制御(重大)
overrideAccess: true はすべてのアクセス制御をバイパスします。ユーザーが渡された場合でも同様です。
// ❌ SECURITY BUG: Passes user but bypasses their permissions anyway
await payload.find({
collection: 'posts',
user: someUser,
overrideAccess: true, // Access control is BYPASSED!
})
// ✅ SECURE: Respects the user's permissions — this is the default, overrideAccess can be omitted
await payload.find({
collection: 'posts',
user: someUser,
})
overrideAccess が省略可能な Local API 操作では、デフォルトは false です — 明示的に
バイパスしない限り、アクセス制御は尊重されます。
- 省略する、または
overrideAccess: falseを設定する - ユーザーに代わって操作する場合(API ルート、ユーザー向けのサーバー関数、ユーザーとして動作する Webhook)。userを併せて渡してください。 overrideAccess: true- 信頼されたシステム作業(cron ジョブ、シード、マイグレーション、システムタスク、および完全な権限を意図的に付与された独立して認証された Webhook)
習慣や近くの呼び出しをコピーして overrideAccess: true を設定しないでください — この呼び出しが意味する値を選んでください。
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',
overrideAccess: true,
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',
overrideAccess: true,
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',
overrideAccess: true,
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',
overrideAccess: true,
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'
よくある落とし穴
- オプションの
overrideAccessを伴うローカル API 操作は、デフォルトでアクセス制御に従います — 信頼できるサーバーサイドの処理にのみtrueに設定してください - ネストされた操作で
reqが欠落していると、トランザクションの原子性が損なわれます - フックループ — フック内の操作が同じフックを再トリガーする可能性があります。
req.contextフラグを使用してください - フィールドレベルのアクセスは真偽値のみを返し、クエリ制約はありません
- リレーションの深さはデフォルトで 2 です。ID のみにする場合は
depth: 0を設定してください - 下書きステータス — 下書きが有効な場合、
_statusフィールドが自動的に注入されます - 型は開発時 (
autoGenerate) およびpayload build中に自動的に再生成されます —generate:typesを手動で実行することは避けてください - MongoDB のトランザクションにはレプリカセットの設定が必要です
- SQLite のトランザクションはデフォルトで無効です。
transactionOptions: {}で有効にしてください - ポイントフィールドは SQLite ではサポートされていません
- 計算された仮想タイトル —
virtual: trueで設定されたフィールドはadmin.useAsTitleで使用できません。保存されたトップレベルのフィールドを使用してください
ベストプラクティス
コンテンツモデリング
- コンテンツコレクションではデフォルトで
versions: { drafts: true }を有効にし、カスタムのstatusフィールドを追加するのではなく、自動注入される_statusフィールドに依存してください - スラッグには、独自の一意なテキストフィールドを手作りするのではなく、ネイティブの
slugフィールド型を使用してください admin.useAsTitleには保存されたトップレベルのフィールドを使用してください。計算されたvirtual: trueフィールドをタイトルとして使用しないでくださいposition: 'sidebar'は短く一目でわかるフィールド (ステータス、カテゴリ、著者、日付) 用に取っておき、長いフィールド (説明、リッチテキスト) はメインエリアに配置してください
セキュリティ
- 制限的なアクセスをデフォルトとし、徐々に権限を追加してください
- 呼び出しがユーザーのために動作する場合は常に
overrideAccess: falseを使用し、そのuserを渡してください - フィールドレベルのアクセスは真偽値のみを返します (クエリ制約はありません)
- クライアントから提供されたデータを決して信頼しないでください
- データベースのルックアップを避けるために、ロールには
saveToJWT: trueを使用してください
パフォーマンス
- 頻繁にクエリされるフィールドにインデックスを付けてください
- 返されるフィールドを制限するには
selectを使用してください - 過剰なフェッチを防ぐために、リレーションに
maxDepthを設定してください - アクセス制御では、非同期操作よりもクエリ制約を優先してください
- 負荷の高い操作は
req.contextにキャッシュしてください
データ整合性
- フック内のネストされた操作には常に
reqを渡してください - 無限のフックループを防ぐためにコンテキストフラグを使用してください
- MongoDB (レプリカセットが必要) と Postgres でトランザクションを有効にしてください
- データのフォーマットには
beforeValidateを使用してください - ビジネスロジックには
beforeChangeを使用してください
型安全性
dev(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 - プラグインアーキテクチャ、モノレポ構成、パターン、ベストプラクティス