关于 payload
payload 是面向 Next.js 的 TypeScript 优先 CMS 开发技能,供构建内容管理应用的开发者参考。它涵盖内容建模、访问控制、查询、钩子和插件等实现模式。
主要功能
- 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字段,因为它是多余的。对于没有发布/草稿生命周期的集合(例如内部联结表、设置),才跳过版本设置。 - 所有 slug 都使用
slugField(),不要手动编写{ name: 'slug', type: 'text', unique: true }。它会根据标题自动生成 slug、添加重新生成开关,并为你处理唯一性/索引。默认根据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' }
有关所有字段类型(数组、区块、点、联结、虚拟、条件字段等),请参阅 FIELDS.md。
Hook 示例
Hook 位于两个层级之一,二者不能互换。集合 Hook 接收 { doc, data, req, operation, ... },并作用于整个文档。字段 Hook 位于单个字段的 hooks 对象中,接收 { value, siblingData, ... },并返回该字段的新值。计算/虚拟字段、单字段格式化器和单字段访问权限屏蔽都属于字段 Hook;跨字段业务逻辑则属于集合 Hook。
// 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}`],
},
},
],
}
如果有人要求“计算一个字段”或“在 Hook 中填充字段的值”,请在该字段上使用字段级 Hook——不要使用集合级 afterRead 来修改 doc。
有关所有 Hook 模式,请参阅 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(默认值)——你信任的服务端操作(定时任务、系统任务)overrideAccess: false——代表用户执行操作时(API 路由、Webhook)
请参阅 QUERIES.md#access-control-in-local-api。
2. Hook 中的事务失败
Hook 中未传入 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. 无限 Hook 循环
Hook 触发的操作又会触发相同的 Hook,从而形成无限循环。
// ❌ 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会破坏事务的原子性 - Hook 循环——Hook 中的操作可能会再次触发相同的 Hook;使用
req.context标志 - 字段级访问只返回布尔值,不提供查询约束
- 关系深度默认为 2;设置
depth: 0可仅获取 ID - 草稿状态——启用草稿后会自动注入
_status字段 - 类型会在开发期间(
autoGenerate)和payload build期间自动重新生成——避免手动运行generate:types - MongoDB 事务需要副本集配置
- SQLite 事务默认禁用;使用
transactionOptions: {}启用 - SQLite 不支持 Point 字段
最佳实践
内容建模
- 默认在内容集合上启用
versions: { drafts: true };依赖自动注入的_status字段,而不是添加自定义的status字段 - 使用
slugField()创建 slug,而不是手动创建唯一文本字段 - 将
position: 'sidebar'留给简短、便于快速查看的字段(状态、类别、作者、日期);将长字段(描述、富文本)放在主区域
安全
- 默认采用限制性访问控制,再逐步添加权限
- 向 Local API 传入
user时使用overrideAccess: false - 字段级访问只返回布尔值(不提供查询约束)
- 永远不要信任客户端提供的数据
- 对角色使用
saveToJWT: true,避免查询数据库
性能
- 为频繁查询的字段建立索引
- 使用
select限制返回的字段 - 在关系上设置
maxDepth,避免过度获取数据 - 在访问控制中优先使用查询约束,而不是异步操作
- 在
req.context中缓存开销较大的操作
数据完整性
- 在 Hook 中始终向嵌套操作传入
req - 使用上下文标志防止无限 Hook 循环
- 为 MongoDB(需要副本集)和 Postgres 启用事务
- 使用
beforeValidate格式化数据 - 使用
beforeChange处理业务逻辑
类型安全
- 让开发环境(
autoGenerate)和payload build生成类型;仅在两者都未运行时手动运行generate:types - 从生成的
payload-types.ts导入类型 - 为用户对象指定类型:
import type { User } from '@/payload-types' - 使用字段类型守卫进行运行时类型检查
- 将任何 Payload 值提取为具名常量时——无论是集合、字段、Hook、访问控制函数、插件等——都要为其标注对应的 Payload 类型(
CollectionConfig、Field、CollectionBeforeChangeHook、Access、Plugin等),或使用satisfies <Type>。如果没有类型标注,像type: 'text'这样的字符串属性会扩宽为string,导致判别联合类型(Field、CollectionConfig)无法解析。内联字面量会通过上下文类型推断自动获得类型;提取出来的常量则不会。
组织结构
- 将集合放在不同的文件中
- 将访问控制提取到
access/目录 - 将 Hook 提取到
hooks/目录 - 为常见模式使用可复用的字段工厂
- 使用注释说明复杂的访问控制
参考文档
- FIELDS.md - 所有字段类型、验证和管理选项
- FIELD-TYPE-GUARDS.md - 用于运行时字段类型检查和缩小范围的类型守卫
- COLLECTIONS.md - 集合配置、身份验证、上传、草稿和实时预览
- HOOKS.md - 集合钩子、字段钩子和上下文模式
- ACCESS-CONTROL.md - 集合、字段和全局访问控制、RBAC、多租户
- ACCESS-CONTROL-ADVANCED.md - 上下文感知、基于时间和订阅的访问控制、工厂函数和模板
- QUERIES.md - 查询运算符、本地/REST/GraphQL API
- ENDPOINTS.md - 自定义 API 端点:身份验证、辅助工具、请求/响应模式
- ADAPTERS.md - 数据库、存储、电子邮件适配器和事务
- ADVANCED.md - 身份验证、作业、端点、组件、插件和本地化
- PLUGIN-DEVELOPMENT.md - 插件架构、单体仓库结构、模式和最佳实践