返回博客

Payload CMS 快速上手

2025/8/128 分钟阅读

Payload CMS 快速上手

如果你受够了 Strapi 的臃肿和 Sanity 的 GROQ 学习曲线,Payload 可能是那个"刚刚好"的无头 CMS。

一、Payload 是什么

Payload 是一个基于 Node.js 的开源无头 CMS,但它与传统 CMS 有几个本质区别:

  • 代码优先(Code-first):所有配置都用 TypeScript/JavaScript 写,没有点击拖拽的 GUI 配置,版本控制友好
  • 自带管理后台:基于 React + Vite 构建的 Admin UI,无需额外搭建
  • 数据库自由:支持 MongoDB(Mongoose)和 PostgreSQL(Drizzle)
  • 框架集成:官方提供 Next.js 深度集成,可以直接把 CMS 嵌入到 App Router 中
  • 类型安全:从字段定义自动生成 TypeScript 类型,前后端类型一致

简单来说,Payload 是**"一个带有精美管理界面的 Node.js 后端框架"**,而不是一个需要你去适配的黑盒系统。


二、环境准备

Payload 3.0 之后全面拥抱 Next.js,官方推荐的初始化方式也是基于 Next.js:

# 需要 Node.js 18+ 和 pnpm/yarn/npm
npx create-payload-app@latest

初始化向导会询问:

  • Project name:项目名称
  • Database:选择 MongoDB 或 PostgreSQL(推荐 PostgreSQL,生产更稳)
  • Storage:文件存储方式(本地磁盘或 S3/Cloudflare R2 等)
  • Template:Blank(空白)或 Website(预置博客模板)

三、项目结构速览

初始化后的目录结构如下:

my-project/
├── src/
│   ├── app/                 # Next.js App Router
│   │   ├── (payload)/       # Payload 管理后台路由
│   │   └── api/             # API 路由
│   ├── collections/         # 数据集合定义(核心)
│   │   ├── Users.ts
│   │   └── Media.ts
│   ├── payload.config.ts    # Payload 全局配置
│   └── payload-types.ts     # 自动生成的类型文件
├── media/                   # 本地上传的文件
└── docker-compose.yml       # 数据库容器(可选)

核心文件解读

文件作用
payload.config.ts数据库连接、插件、全局设置、集合注册
src/collections/*.ts定义数据模型(类似 Prisma Schema,但更偏向 CMS 语义)
src/payload-types.ts自动生成,包含所有 Collection 和 Global 的 TypeScript 类型

四、核心概念:Collections & Globals

Payload 的数据组织分为两类:

4.1 Collections(集合)

类似数据库表,用于多记录场景:文章、产品、用户、标签等。

// src/collections/Posts.ts
import { CollectionConfig } from 'payload'

export const Posts: CollectionConfig = {
  slug: 'posts',           // API 端点:/api/posts
  admin: {
    useAsTitle: 'title',   // 后台列表用 title 字段作为标题
  },
  access: {
    read: () => true,      // 公开读取
    create: ({ req }) => req.user?.role === 'admin', // 仅管理员可创建
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    {
      name: 'slug',
      type: 'text',
      required: true,
      unique: true,
    },
    {
      name: 'content',
      type: 'richText',    // 富文本编辑器(Lexical)
    },
    {
      name: 'coverImage',
      type: 'upload',
      relationTo: 'media', // 关联到 Media 集合
    },
    {
      name: 'publishedAt',
      type: 'date',
      admin: {
        date: {
          pickerAppearance: 'dayAndTime',
        },
      },
    },
    {
      name: 'status',
      type: 'select',
      options: [
        { label: '草稿', value: 'draft' },
        { label: '已发布', value: 'published' },
      ],
      defaultValue: 'draft',
    },
    {
      name: 'author',
      type: 'relationship',
      relationTo: 'users',   // 关联到 Users 集合
    },
    {
      name: 'tags',
      type: 'relationship',
      relationTo: 'tags',
      hasMany: true,         // 多对多
    },
  ],
}

4.2 Globals(全局配置)

用于单记录场景:网站导航、首页 Banner、SEO 设置、页脚信息等。

// src/globals/Settings.ts
import { GlobalConfig } from 'payload'

export const Settings: GlobalConfig = {
  slug: 'settings',
  access: {
    read: () => true,
  },
  fields: [
    {
      name: 'siteName',
      type: 'text',
    },
    {
      name: 'logo',
      type: 'upload',
      relationTo: 'media',
    },
    {
      name: 'socialLinks',
      type: 'array',        // 数组字段
      fields: [
        {
          name: 'platform',
          type: 'select',
          options: ['twitter', 'github', 'weibo'],
        },
        {
          name: 'url',
          type: 'text',
        },
      ],
    },
  ],
}

4.3 注册到配置

// src/payload.config.ts
import { buildConfig } from 'payload'
import { Posts } from './collections/Posts'
import { Tags } from './collections/Tags'
import { Media } from './collections/Media'
import { Settings } from './globals/Settings'

export default buildConfig({
  // ...其他配置
  collections: [Posts, Tags, Media],
  globals: [Settings],
})

修改后重启服务,Payload 会自动:

  1. 生成对应的数据库表/集合
  2. 生成 payload-types.ts 类型文件
  3. 在 Admin UI 中渲染出编辑界面

五、实战:5 分钟搭一个博客

步骤 1:创建 Posts 集合

如上文的 Posts.ts,保存到 src/collections/Posts.ts

步骤 2:创建 Tags 集合

// src/collections/Tags.ts
import { CollectionConfig } from 'payload'

export const Tags: CollectionConfig = {
  slug: 'tags',
  admin: {
    useAsTitle: 'name',
  },
  fields: [
    {
      name: 'name',
      type: 'text',
      required: true,
    },
    {
      name: 'slug',
      type: 'text',
      required: true,
      unique: true,
    },
  ],
}

步骤 3:启动并查看

pnpm dev

访问 http://localhost:3000/admin,使用初始化时创建的账号登录。你会看到左侧导航出现了 PostsTagsMedia 三个菜单。

点击 Posts → Create New,你会发现:

  • 富文本编辑器开箱即用(基于 Lexical,支持 Markdown 快捷输入)
  • 关联字段(author、tags)可以直接搜索选择
  • 上传字段支持拖拽上传图片

六、前端接入:REST 与 GraphQL

Payload 自动为所有 Collections 和 Globals 生成 REST APIGraphQL API

6.1 REST API 示例

# 获取文章列表(带过滤、排序、分页)
GET /api/posts?where[status][equals]=published&sort=-publishedAt&limit=10

# 获取单篇文章
GET /api/posts/:id

# GraphQL Playground
GET /api/graphql

6.2 Next.js 中接入(Server Component)

// app/blog/page.tsx
import { getPayload } from 'payload'
import config from '@/payload.config'

export default async function BlogPage() {
  const payload = await getPayload({ config })
  
  const { docs: posts } = await payload.find({
    collection: 'posts',
    where: {
      status: { equals: 'published' },
    },
    sort: '-publishedAt',
    depth: 1, // 自动填充关联字段(author、tags、coverImage)
  })

  return (
    <main>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          {/* coverImage 已经被 depth:1 填充为完整对象 */}
          {post.coverImage && (
            <img src={post.coverImage.url} alt={post.title} />
          )}
          <p>作者:{typeof post.author === 'object' ? post.author.email : ''}</p>
        </article>
      ))}
    </main>
  )
}

注意payload.find() 返回的数据类型是自动生成的 Post 类型,全程 TypeScript 提示。

6.3 使用 GraphQL

const QUERY = `
  query {
    Posts(where: { status: { equals: published } }, limit: 10) {
      docs {
        id
        title
        slug
        publishedAt
        author {
          email
        }
      }
    }
  }
`

const res = await fetch('/api/graphql', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query: QUERY }),
})

七、进阶技巧

7.1 Hooks:数据生命周期

在数据创建/更新/删除前后插入自定义逻辑:

export const Posts: CollectionConfig = {
  slug: 'posts',
  hooks: {
    beforeChange: [
      ({ data, req, operation }) => {
        // 自动生成 slug
        if (!data.slug && data.title) {
          data.slug = data.title
            .toLowerCase()
            .replace(/\s+/g, '-')
            .replace(/[^\w-]+/g, '')
        }
        return data
      },
    ],
    afterChange: [
      ({ doc, operation }) => {
        // 发布后发送 Webhook 通知搜索引擎
        if (operation === 'create' && doc.status === 'published') {
          // revalidatePath('/blog') 或调用外部 API
        }
      },
    ],
  },
  // ...fields
}

7.2 自定义 Admin 组件

Payload 允许你替换管理后台的几乎任何 UI 组件:

// 在字段中使用自定义组件
{
  name: 'seoScore',
  type: 'ui', // UI 字段,不存储数据
  admin: {
    components: {
      Field: '@/components/SEOScore#SEOScore',
    },
  },
}

7.3 本地化(i18n)

// payload.config.ts
export default buildConfig({
  localization: {
    locales: ['zh', 'en'],
    defaultLocale: 'zh',
    fallback: true,
  },
})

字段级别开启本地化:

{
  name: 'title',
  type: 'text',
  localized: true, // 每种语言独立存储
}

API 调用时通过 ?locale=en 切换语言版本。


八、部署建议

Payload + Next.js 的最佳部署方案:

平台说明
Vercel适合前端 + Serverless API,但长时间运行的任务(如大文件上传)可能超时
Railway / Render容器化部署,适合全栈应用
自有服务器 + Docker生产推荐,配合 docker-compose 编排 Payload + PostgreSQL

生产 checklist

  • 使用 PostgreSQL 而非 MongoDB(Payload 3.0 对 Postgres 支持更成熟)
  • 文件存储迁移到 S3 / Cloudflare R2(使用 @payloadcms/storage-s3
  • 配置 PAYLOAD_SECRET 环境变量
  • 开启数据库连接池(PostgreSQL)
  • 设置 Admin UI 的独立域名或 IP 白名单

九、总结

Payload 的定位非常清晰:给开发者用的 CMS。它不提供"零代码"的虚假承诺,而是用代码优先的方式,让你用熟悉的技术栈(TypeScript + React + Node.js)快速构建内容管理后台。

如果你:

  • 需要深度定制管理后台
  • 希望前后端共享类型定义
  • 已经在使用 Next.js 全栈开发
  • 受够了传统 CMS 的插件生态和版本升级噩梦

那么 Payload 值得一试。


官方资源