mcpField
P
payload

payload

github.com/payloadcms开发45.6k2.7k 次安装今天更新
代码仓库

关于 payload

面向开发者的 Payload 应用开发技能,涵盖 CMS 配置、查询、钩子、访问控制与插件模式,适用于 Next.js 和 TypeScript 项目。

主要功能

  1. 01集合与字段建模
  2. 02草稿、版本与 slug
  3. 03查询与访问控制
  4. 04钩子与事务指南
  5. 05插件和 API 模式

适用场景

  1. 01配置内容集合
  2. 02实现按角色控制内容访问
  3. 03添加钩子与后台任务

安装

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

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

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

包含的文件

除 SKILL.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、身份验证和文件存储。

快速参考

任务解决方案详情
自动生成 slug{ type: 'slug', useAsSlug: 'title' }FIELDS.md#slug-field
按用户限制内容通过查询进行访问控制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.useAsTitle 中COLLECTIONS.md#useastitle
条件字段admin.conditionFIELDS.md#conditional-fields
自定义字段验证validate 函数FIELDS.md#validation
过滤关系列表字段上的 filterOptionsFIELDS.md#relationship
选择特定字段select 参数QUERIES.md#field-selection
自动设置作者/日期beforeChange hookHOOKS.md#collection-hooks
防止 hook 循环req.context 检查HOOKS.md#context
级联删除beforeDelete hookHOOKS.md#collection-hooks
地理空间查询使用 point 字段,配合 near/withinFIELDS.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
插件 hooks在数组中保留现有 hooksPLUGIN-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 使用原生的 slug 字段类型,而不要手动拼凑 { name: 'slug', type: 'text', unique: true }。它会根据源字段自动生成 slug, 添加一个重新生成开关,并默认设置为 required、unique、 index 和 position: 'sidebar'。useAsSlug 是必填的——指定用于生成的 源字段:{ name: 'slug', type: 'slug', useAsSlug: 'title' }。
  • position: 'sidebar' 用于简短的、一目了然的字段——状态、分类、 作者、发布日期。避免将其用于需要水平空间才能 使用的长字段(描述、富文本内容、长文本)。那些字段应放在主 文档区域。
  • 对 admin.useAsTitle 使用存储的顶层字段。 绝不要将 admin.useAsTitle 设置为配置了 virtual: true 的计算字段;这些 字段不可查询,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。

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——绝不要使用会修改 doc 的集合级 afterRead。

有关所有 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',
  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. 本地 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. Hook 中的事务失败

在 Hook 中不使用 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. 无限 Hook 循环

Hook 触发操作、操作又触发同一批 Hook,会形成无限循环。

// ❌ 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 之前生成 import map 和类型。优先使用它而不是直接调用 next build,这样两者都 不会过期。传入 --no-types 可跳过类型生成。
  • 手动生成(payload generate:types)是一种应急手段 —— 仅当开发服务器和构建都不在 流程中时使用(例如一次性脚本,或 CI 中某个不运行 payload build 的步骤之前)。
// 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. 带可选 overrideAccess 的 Local API 操作默认遵循访问控制 —— 仅在受信任的服务端工作中将其设为 true
  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. Point 字段 在 SQLite 中不受支持
  11. 计算型虚拟标题 —— 配置为 virtual: true 的字段不能用于 admin.useAsTitle;请使用存储的顶层字段

最佳实践

内容建模
  • 在内容集合上默认启用 versions: { drafts: true };依赖 自动注入的 _status 字段,而不是添加自定义的 status 字段
  • 使用原生 slug 字段类型来存储 slug,而不是手工打造一个唯一文本字段
  • 为 admin.useAsTitle 使用存储的顶层字段;绝不要使用计算型的 virtual: true 字段作为标题
  • 将 position: 'sidebar' 留给简短、一目了然的字段(状态、分类、 作者、日期);将长字段(描述、富文本)保留在主区域
安全性
  • 默认采用限制性访问,逐步添加权限
  • 只要调用是代表某个用户执行的,就使用 overrideAccess: false,并传入该 user
  • 字段级访问仅返回布尔值(不支持查询约束)
  • 绝不要信任客户端提供的数据
  • 对角色使用 saveToJWT: true 以避免数据库查询
性能
  • 为频繁查询的字段建立索引
  • 使用 select 限制返回的字段
  • 在关系上设置 maxDepth 以防止过度获取
  • 在访问控制中优先使用查询约束而非异步操作
  • 在 req.context 中缓存开销大的操作
数据完整性
  • 始终将 req 传给 Hook 中的嵌套操作
  • 使用 context 标志防止无限 Hook 循环
  • 为 MongoDB(需要副本集)和 Postgres 启用事务
  • 使用 beforeValidate 进行数据格式化
  • 使用 beforeChange 处理业务逻辑
类型安全
  • 让 dev(autoGenerate)和 payload build 生成类型;仅当两者都未运行时,才手动运行 generate:types
  • 从生成的 payload-types.ts 导入类型
  • 为你的 user 对象标注类型:import type { User } from '@/payload-types'
  • 使用字段类型守卫进行运行时类型检查
  • 将任何 Payload 值提取为具名常量时——无论是 collection、field、hook、access 函数、plugin 等——都要用匹配的 Payload 类型(CollectionConfig、Field、CollectionBeforeChangeHook、Access、Plugin……)进行标注,或使用 satisfies <Type>。没有标注时,诸如 type: 'text' 这样的字符串属性会被拓宽为 string,可辨识联合(Field、CollectionConfig)就无法解析。内联字面量通过上下文类型推断自动获得这一效果;提取出的常量则不会。
组织方式
  • 将 collections 放在单独的文件中
  • 将访问控制提取到 access/ 目录
  • 将 hooks 提取到 hooks/ 目录
  • 对常见模式使用可复用的字段工厂
  • 用注释记录复杂的访问控制

参考文档

  • FIELDS.md - 所有字段类型、校验、管理选项
  • FIELD-TYPE-GUARDS.md - 用于运行时字段类型检查和收窄的类型守卫
  • COLLECTIONS.md - Collection 配置、认证、上传、草稿、实时预览
  • HOOKS.md - Collection hooks、field hooks、context 模式
  • ACCESS-CONTROL.md - Collection、field、global 访问控制、RBAC、多租户
  • ACCESS-CONTROL-ADVANCED.md - 上下文感知、基于时间、基于订阅的访问控制、工厂函数、模板
  • QUERIES.md - 查询操作符、Local/REST/GraphQL API
  • ENDPOINTS.md - 自定义 API 端点:认证、辅助函数、请求/响应模式
  • ADAPTERS.md - 数据库、存储、邮件适配器、事务
  • ADVANCED.md - 认证、任务、端点、组件、插件、本地化
  • PLUGIN-DEVELOPMENT.md - 插件架构、monorepo 结构、模式、最佳实践

资源

评论

支持 Markdown