Интеграция Payload CMS с Next.js
Payload 2.x создан с расчётом на интеграцию с Next.js App Router. Монолитный подход — обе системы в одном процессе — исключает сетевые запросы между CMS и фронтендом при серверном рендеринге. Это главное отличие от других headless CMS.
Монолитная архитектура
npx create-payload-app@latest --template website
Структура Next.js + Payload монолита:
my-app/
├── app/
│ ├── (frontend)/ # Публичный сайт
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── [slug]/page.tsx
│ └── (payload)/ # Admin panel
│ └── admin/[[...segments]]/page.tsx
├── collections/
├── globals/
├── payload.config.ts
└── next.config.js
// next.config.js
const { withPayload } = require('@payloadcms/next/withPayload')
module.exports = withPayload({
// ваши настройки Next.js
images: {
remotePatterns: [{ hostname: 'your-cdn.com' }],
},
})
Прямые запросы без HTTP
В Server Components можно вызывать Payload напрямую — без HTTP:
// app/(frontend)/page.tsx
import { getPayload } from 'payload'
import config from '@payload-config'
export default async function HomePage() {
const payload = await getPayload({ config })
// Прямой вызов — нет сетевого запроса
const [homepage, posts, settings] = await Promise.all([
payload.findGlobal({ slug: 'homepage', depth: 2 }),
payload.find({
collection: 'posts',
where: { _status: { equals: 'published' } },
sort: '-publishedAt',
limit: 6,
depth: 1,
}),
payload.findGlobal({ slug: 'settings' }),
])
return (
<>
<Hero data={homepage.hero} />
<FeaturedPosts posts={posts.docs} />
<Footer settings={settings} />
</>
)
}
ISR — Incremental Static Regeneration
// app/(frontend)/posts/[slug]/page.tsx
import { unstable_cache } from 'next/cache'
// Кэшировать запрос с тегом для инвалидации
const getCachedPost = unstable_cache(
async (slug: string) => {
const payload = await getPayload({ config })
const result = await payload.find({
collection: 'posts',
where: { slug: { equals: slug }, _status: { equals: 'published' } },
})
return result.docs[0] || null
},
['post'],
{ tags: ['posts'], revalidate: 3600 }
)
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getCachedPost(params.slug)
if (!post) notFound()
return <Article post={post} />
}
On-demand Revalidation через хуки Payload
// collections/Posts.ts — инвалидация при изменении
hooks: {
afterChange: [
async ({ doc, operation }) => {
if (doc._status === 'published') {
// Инвалидировать кэш по тегу
await revalidateTag('posts')
// Инвалидировать конкретную страницу
await revalidatePath(`/posts/${doc.slug}`)
}
},
],
}
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const secret = req.headers.get('x-revalidate-secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { path, tag } = await req.json()
if (tag) revalidateTag(tag)
if (path) revalidatePath(path)
return NextResponse.json({ revalidated: true })
}
Client-side операции
Авторизация и операции, требующие токена, — через fetch в Client Components:
// app/(frontend)/components/ContactForm.tsx
'use client'
import { useState } from 'react'
export const ContactForm = () => {
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setStatus('loading')
const formData = new FormData(e.currentTarget)
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(Object.fromEntries(formData)),
})
setStatus(res.ok ? 'success' : 'error')
}
return (
<form onSubmit={handleSubmit}>
{/* поля формы */}
<button disabled={status === 'loading'}>
{status === 'loading' ? 'Отправка...' : 'Отправить'}
</button>
{status === 'success' && <p>Заявка отправлена!</p>}
</form>
)
}
TypeScript: автогенерируемые типы
После изменения коллекций — перегенерировать типы:
npm run generate:types
// Использование типов
import type { Post, Page, Media, Settings } from '@/payload-types'
// Полная типизация ответов API
const posts: Post[] = result.docs
const settings: Settings = await payload.findGlobal({ slug: 'settings' })
Сроки
Интеграция Payload с Next.js App Router, настройка ISR, типизация — 2–3 дня при работе с готовыми коллекциями.







