payload by payloadcms/skills
npx skills add https://github.com/payloadcms/skills --skill payloadPayload 是一个基于 Next.js 的原生 CMS,采用 TypeScript 优先的架构,提供管理面板、数据库管理、REST/GraphQL API、身份验证和文件存储功能。
| 任务 | 解决方案 | 详情 |
|---|---|---|
| 自动生成 slug | slugField() | FIELDS.md#slug-field-helper |
| 按用户限制内容 | 带查询的访问控制 | ACCESS-CONTROL.md#row-level-security-with-complex-queries |
| 本地 API 用户操作 | user + overrideAccess: false | QUERIES.md#access-control-in-local-api |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 草稿/发布工作流 | versions: { drafts: true } | COLLECTIONS.md#versioning--drafts |
| 计算字段 | virtual: true 配合 afterRead | FIELDS.md#virtual-fields |
| 条件字段 | admin.condition | FIELDS.md#conditional-fields |
| 自定义字段验证 | validate 函数 | FIELDS.md#text-field |
| 筛选关系列表 | 字段上的 filterOptions | FIELDS.md#relationship |
| 选择特定字段 | select 参数 | QUERIES.md#local-api |
| 自动设置作者/日期 | beforeChange 钩子 | HOOKS.md#collection-hooks |
| 防止钩子循环 | req.context 检查 | HOOKS.md#hook-context |
| 级联删除 | beforeDelete 钩子 | HOOKS.md#collection-hooks |
| 地理空间查询 | point 字段配合 near/within | 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,
}),
})
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' },
],
timestamps: true,
}
更多集合模式(身份验证、上传、草稿、实时预览),请参阅 COLLECTIONS.md。
// 文本字段
{ name: 'title', type: 'text', required: true }
// 关系字段
{ name: 'author', type: 'relationship', relationTo: 'users', required: true }
// 富文本字段
{ name: 'content', type: 'richText', required: true }
// 选择字段
{ name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' }
// 上传字段
{ name: 'image', type: 'upload', relationTo: 'media' }
所有字段类型(数组、区块、点、连接、虚拟、条件等),请参阅 FIELDS.md。
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' }],
}
所有钩子模式,请参阅 HOOKS.md。访问控制,请参阅 ACCESS-CONTROL.md。
import type { Access } from 'payload'
import type { User } from '@/payload-types'
// 类型安全的访问控制
export const adminOnly: Access = ({ req }) => {
const user = req.user as User
return user?.roles?.includes('admin') || false
}
// 行级访问控制
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 },
}
}
// 本地 API
const posts = await payload.find({
collection: 'posts',
where: {
status: { equals: 'published' },
'author.name': { contains: 'john' },
},
depth: 2,
limit: 10,
sort: '-createdAt',
})
// 查询并填充关系
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 2, // 填充关系(默认为 2)
})
// 返回: { author: { id: "user123", name: "John" } }
// 没有 depth 时,关系仅返回 ID
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 0,
})
// 返回: { author: "user123" }
所有查询操作符和 REST/GraphQL 示例,请参阅 QUERIES.md。
// 在 API 路由中 (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)
}
// 在服务器组件中
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>
}
// ✅ 有效:单个字符串
payload.logger.error('Something went wrong')
// ✅ 有效:包含 msg 和 err 的对象
payload.logger.error({ msg: 'Failed to process', err: error })
// ❌ 无效:不要将错误作为第二个参数传递
payload.logger.error('Failed to process', error)
// ❌ 无效:使用 `err` 而不是 `error`,使用 `msg` 而不是 `message`
payload.logger.error({ message: 'Failed', error: error })
默认情况下,本地 API 操作会绕过所有访问控制,即使传递了用户。
// ❌ 安全漏洞:传递了用户但忽略了其权限
await payload.find({
collection: 'posts',
user: someUser, // 访问控制被绕过!
})
// ✅ 安全:实际强制执行用户权限
await payload.find({
collection: 'posts',
user: someUser,
overrideAccess: false, // 访问控制必需
})
何时使用:
overrideAccess: true (默认) - 你信任的服务器端操作(定时任务、系统任务)overrideAccess: false - 代表用户操作时(API 路由、Webhooks)钩子中没有 req 的嵌套操作会破坏事务原子性。
// ❌ 数据损坏风险:单独的事务
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { docId: doc.id },
// 缺少 req - 在单独的事务中运行!
})
},
]
}
// ✅ 原子性:同一事务
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { docId: doc.id },
req, // 保持原子性
})
},
]
}
钩子触发的操作再次触发同一钩子,会造成无限循环。
// ❌ 无限循环
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.update({
collection: 'posts',
id: doc.id,
data: { views: doc.views + 1 },
req,
}) // 再次触发 afterChange!
},
]
}
// ✅ 安全:使用上下文标志
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.config.ts
export default buildConfig({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
// ...
})
// 使用
import type { Post, User } from '@/payload-types'
每周安装量
1.7K
代码仓库
GitHub 星标
48
首次出现
2026 年 1 月 26 日
安全审计
安装于
opencode1.5K
codex1.5K
gemini-cli1.5K
github-copilot1.5K
kimi-cli1.3K
amp1.3K
Payload is a Next.js native CMS with TypeScript-first architecture, providing admin panel, database management, REST/GraphQL APIs, authentication, and file storage.
| Task | Solution | Details |
|---|---|---|
| Auto-generate slugs | slugField() | FIELDS.md#slug-field-helper |
| Restrict content by user | Access control with query | ACCESS-CONTROL.md#row-level-security-with-complex-queries |
| Local API user ops | user + overrideAccess: false | QUERIES.md#access-control-in-local-api |
| Draft/publish workflow | versions: { drafts: true } | COLLECTIONS.md#versioning--drafts |
| Computed fields | virtual: true with afterRead | FIELDS.md#virtual-fields |
| Conditional fields | admin.condition | FIELDS.md#conditional-fields |
| Custom field validation | validate function | FIELDS.md#text-field |
| Filter relationship list | filterOptions on field | FIELDS.md#relationship |
| Select specific fields | select parameter | QUERIES.md#local-api |
| Auto-set author/dates | beforeChange hook | HOOKS.md#collection-hooks |
| Prevent hook loops | req.context check | HOOKS.md#hook-context |
| Cascading deletes | beforeDelete hook | HOOKS.md#collection-hooks |
| Geospatial queries | point field with near/within | FIELDS.md#point-geolocation |
| Reverse relationships | join field type | FIELDS.md#join-fields |
| Next.js revalidation | Context control in afterChange | HOOKS.md#nextjs-revalidation-with-context-control |
| Query by relationship | Nested property syntax | QUERIES.md#nested-properties |
| Complex queries | AND/OR logic | QUERIES.md#andor-logic |
| Transactions | Pass req to operations | ADAPTERS.md#threading-req-through-operations |
| Background jobs | Jobs queue with tasks | ADVANCED.md#jobs-queue |
| Custom API routes | Collection custom endpoints | ADVANCED.md#custom-endpoints |
| Cloud storage | Storage adapter plugins | ADAPTERS.md#storage-adapters |
| Multi-language | localization config + localized: true | ADVANCED.md#localization |
| Create plugin | (options) => (config) => Config | PLUGIN-DEVELOPMENT.md#plugin-architecture |
| Plugin package setup | Package structure with SWC | PLUGIN-DEVELOPMENT.md#plugin-package-structure |
| Add fields to collection | Map collections, spread fields | PLUGIN-DEVELOPMENT.md#adding-fields-to-collections |
| Plugin hooks | Preserve existing hooks in array | PLUGIN-DEVELOPMENT.md#adding-hooks |
| Check field type | Type guard functions | 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,
}),
})
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' },
],
timestamps: true,
}
For more collection patterns (auth, upload, drafts, live preview), see 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 }
// Select
{ name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' }
// Upload
{ name: 'image', type: 'upload', relationTo: 'media' }
For all field types (array, blocks, point, join, virtual, conditional, etc.), see FIELDS.md.
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' }],
}
For all hook patterns, see HOOKS.md. For access control, see 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" }
For all query operators and REST/GraphQL examples, see QUERIES.md.
// 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>
}
// ✅ Valid: single string
payload.logger.error('Something went wrong')
// ✅ Valid: object with msg and err
payload.logger.error({ msg: 'Failed to process', err: error })
// ❌ Invalid: don't pass error as second argument
payload.logger.error('Failed to process', error)
// ❌ Invalid: use `err` not `error`, use `msg` not `message`
payload.logger.error({ message: 'Failed', error: error })
By default, Local API operations bypass ALL access control , even when passing a user.
// ❌ 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
})
When to use each:
overrideAccess: true (default) - Server-side operations you trust (cron jobs, system tasks)overrideAccess: false - When operating on behalf of a user (API routes, webhooks)See QUERIES.md#access-control-in-local-api.
Nested operations in hooks withoutreq break transaction atomicity.
// ❌ 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
})
},
]
}
See ADAPTERS.md#threading-req-through-operations.
Hooks triggering operations that trigger the same hooks create infinite loops.
// ❌ 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,
})
},
]
}
See 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.config.ts
export default buildConfig({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
// ...
})
// Usage
import type { Post, User } from '@/payload-types'
Weekly Installs
1.7K
Repository
GitHub Stars
48
First Seen
Jan 26, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode1.5K
codex1.5K
gemini-cli1.5K
github-copilot1.5K
kimi-cli1.3K
amp1.3K
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
102,200 周安装