Skip to main content

nextjs-integration

This skill should be used when the user asks about "Payload with Next.js", "getPayload in server component", "Payload App Router", "Payload route groups", "Payload live preview Next.js", "revalidate Payload page", "Payload server actions", "Payload draft mode", "Payload Next.js cache", or needs to wire PayloadCMS into a Next.js 15/16 App Router frontend.

Source facts

Repository
Agents-Store/opencode-plugins
Last source activity
October 3, 2026 at 23:16
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
nextjs-integration
description
This skill should be used when the user asks about "Payload with Next.js", "getPayload in server component", "Payload App Router", "Payload route groups", "Payload live preview Next.js", "revalidate Payload page", "Payload server actions", "Payload draft mode", "Payload Next.js cache", or needs to wire PayloadCMS into a Next.js 15/16 App Router frontend.
# PayloadCMS — Next.js Integration PayloadCMS v3 is **Next.js native**. The admin panel ships as Next.js App Router routes, and your frontend lives in the same project. This skill covers data fetching in Server Components, cache invalidation, draft mode, live preview, server actions, and route groups. ## Project Layout (created by `create-payload-app`) ```txt src/ ├── app/ │ ├── (frontend)/ # Your public site │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── posts/ │ │ │ ├── page.tsx │ │ │ └── [slug]/page.tsx │ │ └── api/ # Public API routes (optional) │ │ └── webhook/route.ts │ └── (payload)/ # Payload admin + REST + GraphQL │ ├── admin/ │ │ └── [[...segments]]/page.tsx │ ├── api/ │ │ └── [...slug]/route.ts │ ├── api/graphql/route.ts │ ├── layout.tsx │ └── custom.scss ├── collections/ ├── payload.config.ts └── payload-types.ts # Generated by `pnpm generate:types` ``` Two route groups keep frontend and admin layouts separate. **Don't move `(payload)`** — Payload's import map and Tailwind config assume that exact path. ## Reading Data in Server Components The Local API is the recommended path — zero network overhead, full type safety: ```tsx // app/(frontend)/posts/page.tsx import { getPayload } from 'payload' import config from '@payload-config' import Link from 'next/link' export default async function PostsList() { const payload = await getPayload({ config }) const { docs: posts } = await payload.find({ collection: 'posts', where: { _status: { equals: 'published' } }, sort: '-publishedAt', limit: 20, depth: 1, }) return ( <ul> {posts.map((p) => ( <li key={p.id}> <Link href={`/posts/${p.slug}`}>{p.title}</Link> </li> ))} </ul> ) } ``` `@payload-config` is the path alias the scaffolder adds to `tsconfig.json` (`compilerOptions.paths`). It resolves to `src/payload.config.ts`. ## Generated Types After every collection / global change: ```bash pnpm generate:types ``` Import generated types in app code: ```ts import type { Post, User, Page } from '@/payload-types' ``` This makes the Local API responses fully typed: `payload.find({ collection: 'posts' })` returns `PaginatedDocs<Post>`. ## Single Document by Slug ```tsx // app/(frontend)/posts/[slug]/page.tsx import { getPayload } from 'payload' import config from '@payload-config' import { notFound } from 'next/navigation' import { draftMode } from 'next/headers' export default async function PostPage({ params }: { params: { slug: string } }) { const { isEnabled: isDraft } = await draftMode() const payload = await getPayload({ config }) const { docs } = await payload.find({ collection: 'posts', where: { slug: { equals: params.slug } }, draft: isDraft, limit: 1, depth: 2, }) const post = docs[0] if (!post) return notFound() return ( <article> <h1>{post.title}</h1> {/* render content via RichText (see lexical-editor skill) */} </article> ) } ``` ## Static Generation + ISR For mostly-static pages, statically generate at build and revalidate via hooks: ```tsx // app/(frontend)/posts/[slug]/page.tsx export const revalidate = 3600 // 1 hour export async function generateStaticParams() { const payload = await getPayload({ config }) const { docs } = await payload.find({ collection: 'posts', where: { _status: { equals: 'published' } }, limit: 1000, select: { slug: true }, }) return docs.map((p) => ({ slug: p.slug })) } ``` ## Revalidation on Save Wire Payload's `afterChange` and `afterDelete` hooks to `revalidatePath` / `revalidateTag`: ```ts // src/collections/Posts.ts import { revalidatePath, revalidateTag } from 'next/cache' export const Posts: CollectionConfig = { slug: 'posts', // … hooks: { afterChange: [ ({ doc, previousDoc, req: { context } }) => { if (context?.disableRevalidate) return doc if (doc._status === 'published') { revalidatePath(`/posts/${doc.slug}`) revalidateTag('posts') } // If slug changed, also kill the old path if (previousDoc?.slug && previousDoc.slug !== doc.slug) { revalidatePath(`/posts/${previousDoc.slug}`) } return doc }, ], afterDelete: [ ({ doc }) => { revalidatePath(`/posts/${doc.slug}`) revalidateTag('posts') }, ], }, } ``` To bulk-write without firing revalidation: ```ts await payload.update({ collection: 'posts', where: {/*…*/}, data: {/*…*/}, context: { disableRevalidate: true }, req }) ``` ## Draft Mode Lets editors preview unpublished content from the live frontend. 1. Add a preview URL on the collection: ```ts admin: { preview: (doc) => `${process.env.NEXT_PUBLIC_SITE_URL}/api/preview?slug=${doc.slug}&secret=${process.env.PREVIEW_SECRET}&path=/posts/${doc.slug}`, } ``` 2. Wire a Next.js route handler to enable draft mode: ```ts // app/(frontend)/api/preview/route.ts import { draftMode } from 'next/headers' import { redirect } from 'next/navigation' export async function GET(req: Request) { const { searchParams } = new URL(req.url) const secret = searchParams.get('secret') const path = searchParams.get('path') || '/' if (secret !== process.env.PREVIEW_SECRET) { return new Response('Invalid token', { status: 401 }) } const dm = await draftMode() dm.enable() redirect(path) } ``` 3. Exit: ```ts // app/(frontend)/api/exit-preview/route.ts import { draftMode } from 'next/headers' import { redirect } from 'next/navigation' export async function GET() { ;(await draftMode()).disable() redirect('/') } ``` 4. Read drafts in your page via `draftMode()` + `draft: true` (see Single Document above). ## Live Preview (real-time) ```ts admin: { livePreview: { url: ({ data }) => `${process.env.NEXT_PUBLIC_SITE_URL}/posts/${data.slug}?live-preview=true`, breakpoints: [ { name: 'mobile', width: 375, height: 667, label: 'Mobile' }, { name: 'desktop', width: 1440, height: 900, label: 'Desktop' }, ], }, } ``` On the frontend, use Payload's hook to receive live-edit messages from the iframe parent: ```tsx // app/(frontend)/posts/[slug]/page-live-preview.client.tsx 'use client' import { RefreshRouteOnSave } from '@payloadcms/live-preview-react' export function LivePreview() { return <RefreshRouteOnSave /> } ``` Add it to the page only when in live-preview mode. ## Server Actions Server actions can call the Local API directly: ```tsx // app/(frontend)/comments/new/page.tsx 'use server' import { getPayload } from 'payload' import config from '@payload-config' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' export async function submitComment(formData: FormData) { const payload = await getPayload({ config }) await payload.create({ collection: 'comments', data: { author: formData.get('author') as string, body: formData.get('body') as string, }, }) revalidatePath('/comments') redirect('/comments/thanks') } ``` For server actions handling **authenticated** user requests, propagate auth via `payload.auth({ headers: request.headers })` and use `overrideAccess: false`. ## Caching Strategy - **Local API** does NOT auto-cache. Wrap fetches with `next.cache` or `unstable_cache`: ```ts import { unstable_cache } from 'next/cache' export const getPublishedPosts = unstable_cache( async () => { const payload = await getPayload({ config }) return payload.find({ collection: 'posts', where: { _status: { equals: 'published' } } }) }, ['posts', 'published'], { tags: ['posts'], revalidate: 60 }, ) ``` - Invalidate cached fetches with `revalidateTag('posts')` from Payload hooks. - Don't wrap admin / REST endpoints — they manage their own caching. ## Custom Endpoints Add a custom server-side endpoint to the Payload `api/`: ```ts // payload.config.ts endpoints: [ { path: '/health', method: 'get', handler: () => Response.json({ ok: true, ts: new Date().toISOString() }), }, ], ``` Accessible at `/api/health`. See the `collections` skill for collection-scoped endpoints. ## Auth in API Routes ```ts // app/(frontend)/api/me/route.ts import { getPayload } from 'payload' import config from '@payload-config' export async function GET(req: Request) { const payload = await getPayload({ config }) const { user } = await payload.auth({ headers: req.headers }) if (!user) return new Response('Unauthorized', { status: 401 }) return Response.json({ user }) } ``` ## Deployment Notes - **Next.js versions** — `@payloadcms/next` 3.90.x requires `next >=16.3.3 <17` (the scaffolded templates pin 16.3.3), or one of the patched 15.x lines: `>=15.2.9 <15.3.0`, `>=15.3.9 <15.4.0`, `>=15.4.11 <15.5.0`. **15.5.x is not supported**, nor are 16.0 – 16.3.2, nor Next 14. Check the live range with `npm view @payloadcms/next@latest peerDependencies.next` before pinning, and move to 16.3.3+ when you upgrade Payload — the peer range moves with every Payload release. - **Vercel** — works out of the box. Use `@payloadcms/db-vercel-postgres` and `@payloadcms/storage-vercel-blob`. - **Self-host (Node)** — `pnpm build && pnpm start`. Static assets from `public/` go behind your reverse proxy (Caddy / nginx). - **Payload Cloud** — managed hosting from the Payload team. One-click deploy from GitHub. - **Edge runtime** — admin and Payload routes require Node runtime. Don't set `export const runtime = 'edge'` on `(payload)` routes. ## See Also - The `setup` skill — initial project creation. - The `queries` skill — Local API method reference. - The `hooks` skill — afterChange / afterDelete patterns. - The `lexical-editor` skill — rendering `richText` in server components. - The `jobs-queue` skill — scheduling jobs via Vercel cron.
View on GitHub