Інтеграція 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/[[...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
У серверних компонентах можна викликати 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 })
}
Клієнтські операції
Автентифікація та операції, які вимагають токена, обробляються через 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 дні з існуючими колекціями.







