mcpField
P
payload

payload

github.com/payloadcms开发1706.9k 次安装16 周前更新
代码仓库

关于 payload

payload 是面向 Next.js 的 TypeScript 优先 CMS 开发技能,供构建内容管理应用的开发者参考。它涵盖内容建模、访问控制、查询、钩子和插件等实现模式。

主要功能

  1. 01配置内容集合与字段
  2. 02管理草稿、版本和本地化
  3. 03设置访问控制与查询
  4. 04编写钩子和自定义 API 路由
  5. 05开发插件并生成类型

适用场景

  1. 01创建带草稿发布流程的 CMS
  2. 02按用户权限限制内容访问
  3. 03实现字段计算与内容钩子
  4. 04扩展 Payload 插件功能

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/payloadcms/skills --skill payload

安装后在 Claude Code 中输入/payload确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
reference/
ACCESS-CONTROL-ADVANCED.md
ACCESS-CONTROL.md
ADAPTERS.md
ADVANCED.md
COLLECTIONS.md
ENDPOINTS.md
FIELD-TYPE-GUARDS.md
FIELDS.md
HOOKS.md
PLUGIN-DEVELOPMENT.md
QUERIES.md

详细介绍

Payload 应用开发

Payload 是一个 Next.js 原生 CMS,采用 TypeScript 优先的架构,提供管理面板、数据库管理、REST/GraphQL API、身份验证和文件存储。

快速参考

任务解决方案详情
自动生成 slugslugField()FIELDS.md#slug-field-helper
按用户限制内容使用查询进行访问控制ACCESS-CONTROL.md#row-level-security-with-complex-queries
本地 API 用户操作user + overrideAccess: falseQUERIES.md#access-control-in-local-api
草稿/发布工作流versions: { drafts: true }COLLECTIONS.md#versioning--drafts
计算字段使用 virtual: true,并通过返回值的 字段级 hooks.afterRead 实现FIELDS.md#virtual-fields
条件字段admin.conditionFIELDS.md#conditional-fields
自定义字段验证validate 函数FIELDS.md#validation
筛选关系列表字段上的 filterOptionsFIELDS.md#relationship
选择特定字段select 参数QUERIES.md#field-selection
自动设置作者/日期beforeChange 钩子HOOKS.md#collection-hooks
防止钩子循环检查 req.contextHOOKS.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: trueADVANCED.md#localization
创建插件(options) => (config) => ConfigPLUGIN-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 都使用 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'

常见问题

  1. Local API 会绕过访问控制,除非你传入 overrideAccess: false
  2. 嵌套操作中缺少 req 会破坏事务的原子性
  3. Hook 循环——Hook 中的操作可能会再次触发相同的 Hook;使用 req.context 标志
  4. 字段级访问只返回布尔值,不提供查询约束
  5. 关系深度默认为 2;设置 depth: 0 可仅获取 ID
  6. 草稿状态——启用草稿后会自动注入 _status 字段
  7. 类型会在开发期间(autoGenerate)和 payload build 期间自动重新生成——避免手动运行 generate:types
  8. MongoDB 事务需要副本集配置
  9. SQLite 事务默认禁用;使用 transactionOptions: {} 启用
  10. 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 - 插件架构、单体仓库结构、模式和最佳实践

资源

评论

支持 Markdown