mcpField
P
payload-cms

payload-cms

github.com/connorads开发18240 次安装今天更新
代码仓库

关于 payload-cms

payload-cms 是为 Payload CMS 开发提供指导的技能,面向使用 Next.js 和 TypeScript 构建 CMS 的开发者。它重点涵盖集合配置、钩子、访问控制、查询及安全实践。

主要功能

  1. 01集合、字段与全局配置模式
  2. 02生命周期钩子与数据处理
  3. 03访问控制和用户权限
  4. 04Local API 查询与关系字段
  5. 05事务完整性和钩子循环防护

适用场景

  1. 01为内容集合配置字段与草稿发布
  2. 02按用户权限限制数据访问
  3. 03在钩子中处理数据并保持事务一致
  4. 04诊断 Local API 的访问控制问题

安装

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

$npx skills add https://github.com/connorads/dotfiles --skill payload-cms

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

包含的文件

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

AGENTS.md
README.md
references/
access-control.md
advanced.md
collections.md
fields.md
hooks.md
queries.md

详细介绍

Payload CMS 开发

Payload 是一款原生支持 Next.js 的 CMS,采用 TypeScript 优先的架构。本技能传授构建集合、钩子、访问控制和查询的专业知识。

心智模型

将 Payload 视为三个相互关联的层:

  1. 配置层 → 集合、全局配置和字段定义你的架构
  2. 钩子层 → 生命周期事件用于转换和验证数据
  3. 访问层 → 函数控制谁可以执行哪些操作

每项操作的流程为:Config → Access Check → Hook Chain → Database → Response Hooks

快速参考

任务解决方案详情
自动生成 slugslugField() 或 beforeChange 钩子[references/fields.md#slug-field]
按用户限制访问使用查询约束的访问控制[references/access-control.md]
带身份验证的 Local APIuser + overrideAccess: false[references/queries.md#local-api]
草稿/发布versions: { drafts: true }[references/collections.md#drafts]
计算字段使用 afterRead 钩子的 virtual: true[references/fields.md#virtual]
条件字段admin.condition[references/fields.md#conditional]
筛选关系字段上的 filterOptions[references/fields.md#relationship]
防止钩子循环req.context 标记[references/hooks.md#context]
事务将 req 传递给所有操作[references/hooks.md#transactions]
后台任务使用任务的任务队列[references/advanced.md#jobs]

快速开始

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'

export default buildConfig({
  admin: { user: 'users' },
  collections: [Users, Media, Posts],
  editor: lexicalEditor(),
  secret: process.env.PAYLOAD_SECRET,
  typescript: { outputFile: 'payload-types.ts' },
  db: mongooseAdapter({ url: process.env.DATABASE_URL }),
})

核心模式

集合定义
import type { CollectionConfig } from 'payload'

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    useAsTitle: 'title',
    defaultColumns: ['title', 'author', 'status', 'createdAt'],
  },
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'slug', type: 'text', unique: true, index: true },
    { name: 'content', type: 'richText' },
    { name: 'author', type: 'relationship', relationTo: 'users' },
    { name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' },
  ],
  timestamps: true,
}
钩子模式(自动生成 slug)
export const Posts: CollectionConfig = {
  slug: 'posts',
  hooks: {
    beforeChange: [
      async ({ data, operation }) => {
        if (operation === 'create' && data.title) {
          data.slug = data.title.toLowerCase().replace(/\s+/g, '-')
        }
        return data
      },
    ],
  },
  fields: [{ name: 'title', type: 'text', required: true }],
}
访问控制模式
import type { Access } from 'payload'

// Type-safe: admin-only access
export const adminOnly: Access = ({ req }) => {
  return req.user?.roles?.includes('admin') ?? false
}

// Row-level: users see only their own posts
export const ownPostsOnly: Access = ({ req }) => {
  if (!req.user) return false
  if (req.user.roles?.includes('admin')) return true
  return { author: { equals: req.user.id } }
}
查询模式
// Local API with access control
const posts = await payload.find({
  collection: 'posts',
  where: {
    status: { equals: 'published' },
    'author.name': { contains: 'john' },
  },
  depth: 2,
  limit: 10,
  sort: '-createdAt',
  user: req.user,
  overrideAccess: false, // CRITICAL: enforce permissions
})

关键安全规则

1. Local API 访问控制

默认行为会绕过所有访问控制。 这是最常见的安全错误。

// ❌ SECURITY BUG: Access control bypassed even with user
await payload.find({ collection: 'posts', user: someUser })

// ✅ SECURE: Explicitly enforce permissions
await payload.find({
  collection: 'posts',
  user: someUser,
  overrideAccess: false, // REQUIRED
})

规则: 任何代表用户执行的操作都应使用 overrideAccess: false。

2. 事务完整性

不带 req 的操作会在独立事务中运行。

// ❌ DATA CORRUPTION: Separate transaction
hooks: {
  afterChange: [async ({ doc, req }) => {
    await req.payload.create({
      collection: 'audit-log',
      data: { docId: doc.id },
      // Missing req - breaks atomicity!
    })
  }]
}

// ✅ ATOMIC: Same transaction
hooks: {
  afterChange: [async ({ doc, req }) => {
    await req.payload.create({
      collection: 'audit-log',
      data: { docId: doc.id },
      req, // Maintains transaction
    })
  }]
}

规则: 在钩子中的嵌套操作里始终传递 req。

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: Context flag breaks the loop
hooks: {
  afterChange: [async ({ doc, req, context }) => {
    if (context.skipViewUpdate) return
    await req.payload.update({
      collection: 'posts',
      id: doc.id,
      data: { views: doc.views + 1 },
      req,
      context: { skipViewUpdate: true },
    })
  }]
}

项目结构

src/
├── app/
│   ├── (frontend)/page.tsx
│   └── (payload)/admin/[[...segments]]/page.tsx
├── collections/
│   ├── Posts.ts
│   ├── Media.ts
│   └── Users.ts
├── globals/Header.ts
├── hooks/slugify.ts
└── payload.config.ts

类型生成

架构变更后生成类型:

// payload.config.ts
export default buildConfig({
  typescript: { outputFile: 'payload-types.ts' },
})

// Usage
import type { Post, User } from '@/payload-types'

获取 Payload 实例

// In API routes
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
export default async function Page() {
  const payload = await getPayload({ config })
  const { docs } = await payload.find({ collection: 'posts' })
  return <div>{docs.map(p => <h1 key={p.id}>{p.title}</h1>)}</div>
}

常见字段类型

// Text
{ name: 'title', type: 'text', required: true }

// Relationship
{ name: 'author', type: 'relationship', relationTo: 'users' }

// Rich text
{ name: 'content', type: 'richText' }

// Select
{ name: 'status', type: 'select', options: ['draft', 'published'] }

// Upload
{ name: 'image', type: 'upload', relationTo: 'media' }

// Array
{
  name: 'tags',
  type: 'array',
  fields: [{ name: 'tag', type: 'text' }],
}

// Blocks (polymorphic content)
{
  name: 'layout',
  type: 'blocks',
  blocks: [HeroBlock, ContentBlock, CTABlock],
}

决策框架

选择方案时:

场景方案
保存前转换数据beforeChange 钩子
读取后转换数据afterRead 钩子
执行业务规则访问控制函数
复杂验证字段上的 validate 函数
计算显示值带 afterRead 的虚拟字段
关联文档列表join 字段类型
副作用(电子邮件、webhook)带上下文防护的 afterChange 钩子
数据库级约束带 unique: true 或 index: true 的字段

质量检查

优质的 Payload 代码:

  • 所有带用户上下文的 Local API 调用都使用 overrideAccess: false
  • 所有钩子操作都传递 req,以确保事务完整性
  • 递归钩子使用 context 标记
  • 已生成类型,并从 payload-types.ts 导入
  • 访问控制函数使用 Access 类型进行类型标注
  • 集合设置了有意义的 admin.useAsTitle

参考文档

如需了解详细模式,请参阅:

Resources

评论

支持 Markdown