Payload CMS Website Development
Payload CMS — headless CMS on Node.js with TypeScript-first approach. Configuration is code, not GUI: collections, fields, hooks, access control described in TypeScript files. This means type safety, IDE refactoring, code review in git. Database — MongoDB or PostgreSQL via Drizzle ORM. Admin panel auto-generates from config.
Project Architecture
Payload works as monolith (CMS + frontend in one Next.js app) or standalone API:
Monolith (recommended):
├── app/ # Next.js App Router
│ ├── (frontend)/ # Public pages
│ └── (payload)/ # Admin panel: /admin
├── payload.config.ts # CMS Configuration
├── collections/ # Content types
└── globals/ # Global settings
Project Creation
npx create-payload-app@latest my-site
# Select: template = website, database = PostgreSQL or MongoDB
cd my-site
npm install
npm run dev
# Admin: http://localhost:3000/admin
Collections
Collection — content type with fields:
// collections/Posts.ts
import { CollectionConfig } from 'payload/types'
const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'status', 'publishedAt'],
},
access: {
read: () => true,
create: ({ req }) => req.user?.role === 'admin',
update: ({ req }) => req.user?.role === 'admin',
},
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', unique: true },
{ name: 'content', type: 'richText' },
{ name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' },
{ name: 'publishedAt', type: 'date' },
{ name: 'featuredImage', type: 'upload', relationTo: 'media' },
],
hooks: {
beforeChange: [
({ data }) => {
if (!data.slug && data.title) {
data.slug = data.title.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '')
}
return data
}
],
},
}
export default Posts
Rendering in Next.js
// app/(frontend)/[slug]/page.tsx
import { getPayloadHMR } from '@payloadcms/next/utilities'
import configPromise from '@payload-config'
import { notFound } from 'next/navigation'
export default async function Page({ params }: { params: { slug: string } }) {
const payload = await getPayloadHMR({ config: configPromise })
const result = await payload.find({
collection: 'pages',
where: { slug: { equals: params.slug }, status: { equals: 'published' } },
limit: 1,
})
const page = result.docs[0]
if (!page) notFound()
return (
<main>
<h1>{page.title}</h1>
<RichText content={page.content} />
</main>
)
}
Typical Tech Stack
| Layer | Technology |
|---|---|
| CMS + Admin | Payload CMS 2.x |
| Frontend | Next.js 14 App Router |
| Database | PostgreSQL + Drizzle |
| Media | Cloudflare R2 / AWS S3 |
| Search | Payload built-in / Algolia |
| Resend / Nodemailer | |
| Deploy | Vercel / Railway |
Timeline
Basic site with 3–5 content types: 2–3 weeks. Complex project with custom fields, i18n: 4–8 weeks.







